sozday-rabochee-veb-prilozhe/index.html

256 lines
16 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>
<!-- Дизайн: Стандартный дизайн. Сохраняй все пять CSS; подробности в design.md. -->
<html lang="ru" data-theme="light">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width,initial-scale=1">
<title>КФУ — финансовая устойчивость</title>
<link rel="stylesheet" href="design-system/kt-ai-fonts.css">
<link rel="stylesheet" href="design-system/kt-ai-tokens.css">
<link rel="stylesheet" href="design-system/kt-ai-components.css">
<link rel="stylesheet" href="design-system/kt-ai-page.css">
<link rel="stylesheet" href="design-system/vibe-theme.css">
<script src="design-system/icons/kt-ai-icons.js"></script>
<script src="vendor/xlsx.full.min.js"></script>
<style>
@layer product {
[hidden] { display: none !important; }
a.kt-ai-btn { text-decoration: none; }
.kfu-shell { display: grid; grid-template-columns: var(--kt-ai-sidebar-width) minmax(0, 1fr); min-height: 100dvh; }
.kfu-main { min-width: 0; background: var(--kt-ai-bg); display: flex; flex-direction: column; }
.kfu-content { box-sizing: border-box; width: 100%; min-width: 0; padding: var(--kt-ai-space-8) var(--kt-ai-space-8) var(--kt-ai-space-11); max-width: 1440px; margin: 0 auto; }
.kfu-title-row { display: flex; align-items: baseline; gap: var(--kt-ai-space-4); margin-bottom: var(--kt-ai-space-5); flex-wrap: wrap; }
.kfu-title-row h1 { font-size: var(--kt-ai-text-xl); font-weight: 400; margin: 0; letter-spacing: -.02em; }
.kfu-title-row .kt-ai-meta { white-space: nowrap; }
.kfu-actions { display: flex; gap: var(--kt-ai-space-3); flex-wrap: wrap; margin-bottom: var(--kt-ai-space-6); }
.dropzone { display: inline-flex; align-items: center; gap: var(--kt-ai-space-4); padding: 10px 16px;
border: 1.5px dashed var(--kt-ai-border-strong); border-radius: var(--vibe-card-radius);
background: var(--kt-ai-bg-elevated); color: var(--kt-ai-fg-muted); cursor: pointer; transition: border-color .15s, background .15s; }
.dropzone:hover { border-color: var(--kt-ai-primary); background: var(--kt-ai-bg-hover); color: var(--kt-ai-fg); }
.dropzone input[type=file] { display: none; }
.kfu-kpis { display: grid; grid-template-columns: repeat(auto-fit, minmax(170px, 1fr)); gap: var(--kt-ai-space-4); margin: 0 0 var(--kt-ai-space-6); }
.kfu-kpis .kt-ai-kpi { background: var(--kt-ai-card-bg); border: 1px solid var(--kt-ai-card-border); border-radius: var(--vibe-card-radius); padding: 12px 14px; }
.kfu-kpis .kt-ai-kpi .label { font-size: 10px; }
.kfu-kpis .kt-ai-kpi .value { font-size: var(--kt-ai-text-xl); }
.kfu-toolbar { display: flex; gap: var(--kt-ai-space-3); flex-wrap: wrap; align-items: center; margin-bottom: var(--kt-ai-space-4); }
.kfu-toolbar .kt-ai-search { min-width: 240px; }
.kfu-toolbar .kt-ai-select { min-width: 150px; width: auto; }
.kfu-toolbar .kt-ai-checkbox { display: inline-flex; align-items: center; gap: 6px; font-size: var(--kt-ai-text-sm); color: var(--kt-ai-fg-muted); }
.kfu-table-wrap { box-sizing: border-box; max-width: 100%; background: var(--kt-ai-card-bg); border: 1px solid var(--kt-ai-card-border); border-radius: var(--vibe-card-radius);
overflow-x: auto; box-shadow: var(--kt-ai-card-shadow); }
.kfu-table { width: 100%; border-collapse: collapse; table-layout: fixed; }
.kfu-table colgroup col:nth-child(1) { width: 34%; }
.kfu-table colgroup col:nth-child(2) { width: 16%; }
.kfu-table colgroup col:nth-child(3) { width: 17%; }
.kfu-table colgroup col:nth-child(4) { width: 17%; }
.kfu-table colgroup col:nth-child(5) { width: 16%; }
.kfu-table th { text-align: right; padding: 10px 10px; font-size: var(--kt-ai-text-xs); font-weight: 600; color: var(--kt-ai-fg-muted);
text-transform: uppercase; letter-spacing: .4px; border-bottom: 1px solid var(--kt-ai-divider); white-space: nowrap; cursor: pointer; user-select: none; overflow: hidden; text-overflow: ellipsis; }
.kfu-table th:first-child, .kfu-table td:first-child { text-align: left; }
.kfu-table th .th-sub { display: block; font-weight: 400; text-transform: none; letter-spacing: 0; color: var(--kt-ai-fg-faint); font-size: 10px; white-space: normal; }
.kfu-table th.sorted-asc::after { content: " ↑"; color: var(--kt-ai-link); }
.kfu-table th.sorted-desc::after { content: " ↓"; color: var(--kt-ai-link); }
.kfu-table td { padding: 9px 10px; border-bottom: 1px solid var(--kt-ai-divider); font-size: var(--kt-ai-text-sm); text-align: right;
font-variant-numeric: tabular-nums; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
.kfu-table td:first-child { text-align: left; }
.kfu-table tbody tr { cursor: pointer; transition: background .1s; }
.kfu-table tbody tr:hover { background: var(--kt-ai-bg-hover); }
.kfu-table tbody tr.is-group td { background: var(--kt-ai-bg-soft); font-weight: 600; }
.kfu-table td .none { color: var(--kt-ai-fg-faint); }
.kfu-table td .inf { color: var(--kt-ai-link); font-weight: 600; }
.zone-pill { display: inline-flex; align-items: center; gap: 5px; padding: 2px 10px; border-radius: var(--vibe-button-radius);
font-size: var(--kt-ai-text-xs); font-weight: 600; }
.zone-pill::before { content: ""; width: 7px; height: 7px; border-radius: 50%; background: currentColor; }
.zone-pill.z-green { background: var(--kt-ai-status-ok-bg); color: var(--kt-ai-status-ok-fg); }
.zone-pill.z-yellow { background: var(--kt-ai-status-warn-bg); color: var(--kt-ai-status-warn-fg); }
.zone-pill.z-red { background: var(--kt-ai-status-risk-bg); color: var(--kt-ai-status-risk-fg); }
.zone-pill.z-nodefault { background: var(--kt-ai-bg-active); color: var(--kt-ai-fg-muted); }
.zone-pill.z-nodata { background: transparent; border: 1px dashed var(--kt-ai-border-strong); color: var(--kt-ai-fg-muted); }
.kfu-empty { padding: 48px 24px; text-align: center; color: var(--kt-ai-fg-muted); }
.kfu-empty svg { width: 32px; height: 32px; margin-bottom: 12px; opacity: .5; }
.kfu-drawer { position: fixed; top: 0; right: 0; bottom: 0; width: 520px; max-width: 94vw; background: var(--kt-ai-bg-elevated);
border-left: 1px solid var(--kt-ai-border); box-shadow: -8px 0 24px rgb(12 10 9 / .08); z-index: 100;
padding: var(--kt-ai-space-7); overflow-y: auto; display: none; }
.kfu-drawer.open { display: block; animation: kfuSlide .18s ease-out; }
@keyframes kfuSlide { from { transform: translateX(24px); opacity: .4; } to { transform: none; opacity: 1; } }
.kfu-drawer-head { display: flex; align-items: flex-start; gap: var(--kt-ai-space-3); margin-bottom: var(--kt-ai-space-5); }
.kfu-drawer-head h2 { font-size: var(--kt-ai-text-lg); font-weight: 500; margin: 0 0 4px; letter-spacing: -.01em; }
.kfu-drawer-head .kt-ai-icon-button { margin-left: auto; }
.kfu-drawer-section { margin-bottom: var(--kt-ai-space-6); }
.kfu-drawer-section h3 { font-size: var(--kt-ai-text-xs); font-weight: 700; text-transform: uppercase; letter-spacing: .6px;
color: var(--kt-ai-fg-muted); margin: 0 0 10px; }
.kfu-metrics { display: grid; grid-template-columns: repeat(2, 1fr); gap: var(--kt-ai-space-3); }
.kfu-metric { border: 1px solid var(--kt-ai-card-border); border-radius: var(--vibe-card-radius); padding: 10px 12px; background: var(--kt-ai-bg); }
.kfu-metric .mlabel { font-size: var(--kt-ai-text-xs); color: var(--kt-ai-fg-muted); margin-bottom: 4px; display: flex; align-items: center; justify-content: space-between; gap: 6px; }
.kfu-metric .mval { font-size: 18px; font-weight: 600; font-variant-numeric: tabular-nums; letter-spacing: -.01em; }
.kfu-metric .mhint { font-size: 11px; color: var(--kt-ai-fg-faint); margin-top: 3px; font-variant-numeric: tabular-nums; }
.kfu-formula { font-size: 11px; color: var(--kt-ai-fg-faint); margin-top: 5px; font-family: var(--kt-ai-font-mono); }
.kfu-sources { width: 100%; border-collapse: collapse; font-size: var(--kt-ai-text-xs); }
.kfu-sources td { padding: 5px 0; border-bottom: 1px dashed var(--kt-ai-divider); vertical-align: top; }
.kfu-sources td:last-child { text-align: right; font-family: var(--kt-ai-font-mono); color: var(--kt-ai-fg-muted); white-space: nowrap; padding-left: 12px; }
.kfu-scenario-lbl { font-size: var(--kt-ai-text-sm); color: var(--kt-ai-fg-muted); margin-bottom: 10px; }
.kfu-scenario-row { display: flex; gap: 10px; align-items: stretch; }
.kfu-scenario-row .kt-ai-input { flex: 1; font-variant-numeric: tabular-nums; }
.kfu-delta { font-size: var(--kt-ai-text-sm); margin-top: 10px; padding: 10px 12px; border-radius: var(--vibe-card-radius);
background: var(--kt-ai-status-info-bg); color: var(--kt-ai-status-info-fg); font-variant-numeric: tabular-nums; }
.kfu-delta b { font-weight: 700; }
.kfu-drawer-actions { display: flex; gap: var(--kt-ai-space-3); margin-top: var(--kt-ai-space-6); flex-wrap: wrap; }
.kfu-drawer-actions .kt-ai-btn { flex: 1; }
.kfu-mock-banner { display: flex; align-items: center; gap: var(--kt-ai-space-4); padding: 10px 16px; margin-bottom: var(--kt-ai-space-5);
border-radius: var(--vibe-card-radius); background: var(--kt-ai-chip-default-bg); color: var(--kt-ai-fg); font-size: var(--kt-ai-text-sm); }
.kfu-mock-banner button { margin-left: auto; }
.kfu-toast-stack { position: fixed; bottom: 20px; right: 20px; z-index: 200; display: flex; flex-direction: column; gap: 8px; }
.kfu-toast { padding: 10px 16px; border-radius: 10px; background: var(--kt-ai-fg); color: var(--kt-ai-bg-elevated);
font-size: var(--kt-ai-text-sm); box-shadow: var(--kt-ai-card-shadow-hover); display: flex; align-items: center; gap: 8px;
animation: kfuToastIn .15s ease-out; }
.kfu-toast.err { background: var(--kt-ai-status-risk-fg); }
@keyframes kfuToastIn { from { transform: translateY(8px); opacity: 0; } to { transform: none; opacity: 1; } }
.kfu-footnote { margin-top: var(--kt-ai-space-5); font-size: 11px; color: var(--kt-ai-fg-faint); }
@media (max-width: 860px) {
.kfu-shell { grid-template-columns: 1fr; }
.kt-ai-sidebar { display: none; }
.kfu-content { padding: var(--kt-ai-space-5); }
.kfu-metrics { grid-template-columns: 1fr; }
}
}
</style>
</head>
<body class="kt-ai-app">
<div class="kfu-shell">
<aside class="kt-ai-sidebar">
<div class="kt-ai-brand-row">
<div style="width:28px;height:28px;border-radius:8px;background:var(--kt-ai-primary);color:var(--kt-ai-fg-on-fill);display:flex;align-items:center;justify-content:center;font-weight:700;font-size:13px">V</div>
<div style="line-height:1.15">
<strong style="font-size:13px">Vibe AI</strong><br>
<span style="font-size:11px;color:var(--kt-ai-fg-muted)">КФУ · финансовая устойчивость</span>
</div>
</div>
<nav>
<div class="kt-ai-nav-group">Разделы</div>
<a class="kt-ai-nav-item" data-active="true" href="#top">
<svg class="kt-icon" aria-hidden="true"><use href="#dashboard"></use></svg>
<span>Анализ факта 2025</span>
</a>
</nav>
<div style="margin-top:auto">
<div class="kt-ai-nav-group">Правила</div>
<div style="padding:4px 8px;font-size:11px;color:var(--kt-ai-fg-faint);line-height:1.55">
К1 = (долг + прочие) / EBITDA<br>
К2 = EBITDA / проценты<br>
К3 = (долг + прочие) / капитал<br>
<span style="color:var(--kt-ai-status-ok-fg)">●</span> до цели ·
<span style="color:var(--kt-ai-status-warn-fg)">●</span> до порога ·
<span style="color:var(--kt-ai-status-risk-fg)">●</span> выше порога
</div>
</div>
</aside>
<main class="kfu-main">
<header class="kt-ai-topbar" id="top">
<span style="font-size:var(--kt-ai-text-xs);color:var(--kt-ai-fg-muted)">КФУ · факт 2025</span>
<div style="flex:1"></div>
<button class="kt-ai-btn" data-size="sm" id="btnExport" hidden>
<svg class="kt-icon kt-icon-sm" aria-hidden="true"><use href="#download"></use></svg>
Выгрузить отчёт .xlsx
</button>
</header>
<div class="kfu-content">
<div class="kfu-title-row">
<h1>Финансовая устойчивость</h1>
<span class="kt-ai-meta" id="mSource">файл не загружен</span>
</div>
<div class="kfu-actions">
<label class="dropzone" id="dropzone" title="Выбрать .xlsx с исходными данными">
<svg class="kt-icon kt-icon-lg" aria-hidden="true"><use href="#upload"></use></svg>
<span><b>Загрузить .xlsx</b><br><span class="kt-ai-meta">файл остаётся в браузере, на сервер не отправляется</span></span>
<input type="file" id="fileInput" accept=".xlsx,.xls">
</label>
<button class="kt-ai-btn" data-variant="ghost" id="btnSample">
<svg class="kt-icon kt-icon-sm" aria-hidden="true"><use href="#spreadsheet"></use></svg>
Учебный пример
</button>
</div>
<div class="kfu-mock-banner" id="mockBanner" hidden>
<svg class="kt-icon" aria-hidden="true"><use href="#info"></use></svg>
<span><b>Учебный пример.</b> Все компании и суммы синтетические, созданы только для демонстрации интерфейса.</span>
<button class="kt-ai-btn" data-variant="ghost" data-size="sm" id="btnClearMock">Очистить</button>
</div>
<div class="kfu-kpis" id="kpis" hidden></div>
<div class="kfu-toolbar" id="toolbar" hidden>
<div class="kt-ai-search-field">
<input class="kt-ai-search" id="q" placeholder="Поиск по названию…">
</div>
<select class="kt-ai-select" id="zoneFilter">
<option value="">Все зоны</option>
<option value="green">Зелёные</option>
<option value="yellow">Жёлтые</option>
<option value="red">Красные</option>
</select>
<select class="kt-ai-select" id="normFilter">
<option value="">Нормативы: все</option>
<option value="with">С нормативом</option>
<option value="without">Без норматива</option>
</select>
<label class="kfu-check-wrap" style="margin-left:auto">
<span class="kt-ai-checkbox"><input type="checkbox" id="onlyRisk"></span>
Только красные
</label>
</div>
<div class="kfu-table-wrap" id="tableWrap" hidden>
<table class="kfu-table">
<colgroup>
<col><col><col><col><col>
</colgroup>
<thead>
<tr>
<th data-k="name">Объект</th>
<th>Итог</th>
<th data-k="k1">К1<span class="th-sub">Debt/EBITDA</span></th>
<th data-k="k2">К2<span class="th-sub">EBITDA/Int</span></th>
<th data-k="k3">К3<span class="th-sub">Debt/Equity</span></th>
</tr>
</thead>
<tbody id="tbody"></tbody>
</table>
<div class="kfu-empty" id="emptyState" hidden>
<svg class="kt-icon" aria-hidden="true"><use href="#search"></use></svg>
<div>Ничего не найдено. Измените фильтры или сбросьте запрос.</div>
</div>
</div>
<p class="kfu-footnote" id="footnote"></p>
</div>
</main>
</div>
<aside class="kfu-drawer" id="drawer" role="dialog" aria-modal="false" aria-labelledby="drawerTitle">
<div id="drawerBody"></div>
</aside>
<div class="kfu-toast-stack" id="toasts"></div>
<script src="kfu-client.js"></script>
</body>
</html>