1329 lines
51 KiB
HTML
1329 lines
51 KiB
HTML
<!DOCTYPE html>
|
||
<html lang="ru">
|
||
<head>
|
||
<meta charset="UTF-8" />
|
||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||
<title>Лидпоток — Дашборд</title>
|
||
|
||
<!-- ============================================================
|
||
Прил. Л — Прототип №2: Дашборд
|
||
Версия: v0.1 от 05.05.2026
|
||
Источники: brandbook.md v1.1, narrative §12 (KPI + графики),
|
||
§8.1 (14 статусов с цветами), §21.3 (low balance),
|
||
§1.6 (бизнес-модель), Прил. М §2.6 (виджет напоминаний)
|
||
Стек: HTML + CSS + JS, ApexCharts через CDN cdnjs.cloudflare.com
|
||
============================================================ -->
|
||
|
||
<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=Inter:wght@400;500;600;700&family=JetBrains+Mono:wght@400;500&display=swap&subset=cyrillic" rel="stylesheet" />
|
||
|
||
<!-- ApexCharts через CDN (наш утверждённый source) -->
|
||
<script src="https://cdnjs.cloudflare.com/ajax/libs/apexcharts/3.45.2/apexcharts.min.js"></script>
|
||
|
||
<style>
|
||
/* ============ Брендовые токены ============ */
|
||
:root {
|
||
--teal-900: #04342C;
|
||
--teal-600: #0F6E56;
|
||
--teal-400: #1D9E75;
|
||
--teal-200: #5DCAA5;
|
||
--teal-100: #9FE1CB;
|
||
--teal-50: #E1F5EE;
|
||
|
||
--gray-900: #1A1A1A;
|
||
--gray-700: #444441;
|
||
--gray-500: #888780;
|
||
--gray-300: #B4B2A9;
|
||
--gray-200: #D3D1C7;
|
||
--gray-100: #F1EFE8;
|
||
--gray-50: #FAFAF8;
|
||
--white: #FFFFFF;
|
||
|
||
--success: #22C55E;
|
||
--warning: #F59E0B;
|
||
--danger: #EF4444;
|
||
--info: #3B82F6;
|
||
|
||
/* Цвета 14 статусов воронки (narrative §8.1) */
|
||
--status-new: #3B82F6; /* синий */
|
||
--status-viewed: #8B5CF6; /* фиолетовый */
|
||
--status-worked: #06B6D4; /* голубой */
|
||
--status-base: #888780; /* серый */
|
||
--status-missed: #F59E0B; /* амбер */
|
||
--status-negotiations: #EAB308; /* жёлтый */
|
||
--status-waiting_payment: #C084FC; /* сиреневый */
|
||
--status-partnership: #EC4899; /* розовый */
|
||
--status-paid: #22C55E; /* зелёный */
|
||
--status-closed: #888780; /* серый */
|
||
--status-test_drive: #14B8A6; /* бирюзовый */
|
||
--status-hot: #EF4444; /* красный */
|
||
--status-replacement: #F97316; /* оранжевый */
|
||
--status-final_missed: #444441; /* тёмно-серый */
|
||
|
||
--space-1: 4px; --space-2: 8px; --space-3: 12px;
|
||
--space-4: 16px; --space-6: 24px; --space-8: 32px; --space-12: 48px;
|
||
|
||
--radius-sm: 6px; --radius-md: 8px; --radius-lg: 12px; --radius-pill: 999px;
|
||
|
||
--font-sans: Inter, -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif;
|
||
--font-mono: 'JetBrains Mono', Menlo, Monaco, Consolas, monospace;
|
||
|
||
--shadow-sm: 0 1px 2px rgba(4, 52, 44, 0.06);
|
||
--shadow-md: 0 4px 12px rgba(4, 52, 44, 0.08);
|
||
--shadow-lg: 0 12px 32px rgba(4, 52, 44, 0.12);
|
||
|
||
--sidebar-width: 240px;
|
||
--topbar-height: 60px;
|
||
}
|
||
|
||
/* ============ Reset ============ */
|
||
*, *::before, *::after { box-sizing: border-box; }
|
||
html, body { height: 100%; margin: 0; }
|
||
body {
|
||
font-family: var(--font-sans);
|
||
font-size: 14px;
|
||
line-height: 1.5;
|
||
color: var(--gray-900);
|
||
background: var(--gray-100);
|
||
-webkit-font-smoothing: antialiased;
|
||
-moz-osx-font-smoothing: grayscale;
|
||
}
|
||
button { font: inherit; cursor: pointer; }
|
||
input, select, button { font-family: inherit; }
|
||
a { color: var(--teal-600); text-decoration: none; }
|
||
a:hover { text-decoration: underline; }
|
||
|
||
/* ============ Layout приложения ============ */
|
||
.app {
|
||
display: grid;
|
||
grid-template-columns: var(--sidebar-width) 1fr;
|
||
grid-template-rows: var(--topbar-height) 1fr;
|
||
grid-template-areas:
|
||
"sidebar topbar"
|
||
"sidebar main";
|
||
min-height: 100vh;
|
||
}
|
||
|
||
/* -------- Боковое меню -------- */
|
||
.sidebar {
|
||
grid-area: sidebar;
|
||
background: var(--white);
|
||
border-right: 1px solid var(--gray-200);
|
||
display: flex;
|
||
flex-direction: column;
|
||
position: sticky;
|
||
top: 0;
|
||
height: 100vh;
|
||
}
|
||
.sidebar-brand {
|
||
display: flex;
|
||
align-items: center;
|
||
gap: 12px;
|
||
padding: 18px 20px;
|
||
border-bottom: 1px solid var(--gray-100);
|
||
flex-shrink: 0;
|
||
}
|
||
.brand-mark-circles {
|
||
display: flex; align-items: center; gap: 3px;
|
||
}
|
||
.brand-mark-circles span {
|
||
display: block; border-radius: 50%; background: var(--teal-600);
|
||
}
|
||
.brand-mark-circles span:nth-child(1) { width: 6px; height: 6px; opacity: 0.6; }
|
||
.brand-mark-circles span:nth-child(2) { width: 10px; height: 10px; opacity: 0.8; }
|
||
.brand-mark-circles span:nth-child(3) { width: 14px; height: 14px; }
|
||
.brand-name {
|
||
font-size: 16px;
|
||
font-weight: 600;
|
||
color: var(--teal-900);
|
||
letter-spacing: -0.01em;
|
||
}
|
||
|
||
.sidebar-nav {
|
||
flex: 1;
|
||
padding: 12px 8px;
|
||
overflow-y: auto;
|
||
}
|
||
.sidebar-section {
|
||
font-size: 11px;
|
||
font-weight: 600;
|
||
letter-spacing: 0.06em;
|
||
text-transform: uppercase;
|
||
color: var(--gray-500);
|
||
padding: 16px 12px 8px;
|
||
}
|
||
.sidebar-link {
|
||
display: flex;
|
||
align-items: center;
|
||
gap: 12px;
|
||
padding: 9px 12px;
|
||
border-radius: var(--radius-md);
|
||
color: var(--gray-700);
|
||
font-size: 14px;
|
||
font-weight: 500;
|
||
transition: background-color .15s ease, color .15s ease;
|
||
text-decoration: none;
|
||
margin-bottom: 2px;
|
||
}
|
||
.sidebar-link:hover {
|
||
background: var(--gray-50);
|
||
color: var(--gray-900);
|
||
text-decoration: none;
|
||
}
|
||
.sidebar-link.is-active {
|
||
background: var(--teal-50);
|
||
color: var(--teal-600);
|
||
}
|
||
.sidebar-link svg { flex-shrink: 0; }
|
||
.sidebar-link .badge {
|
||
margin-left: auto;
|
||
background: var(--gray-100);
|
||
color: var(--gray-700);
|
||
padding: 2px 7px;
|
||
border-radius: var(--radius-pill);
|
||
font-size: 11px;
|
||
font-weight: 600;
|
||
}
|
||
.sidebar-link.is-active .badge {
|
||
background: var(--teal-600);
|
||
color: var(--white);
|
||
}
|
||
|
||
.sidebar-foot {
|
||
padding: 16px;
|
||
border-top: 1px solid var(--gray-100);
|
||
flex-shrink: 0;
|
||
}
|
||
.sidebar-user {
|
||
display: flex; align-items: center; gap: 10px;
|
||
}
|
||
.avatar {
|
||
width: 32px; height: 32px;
|
||
border-radius: 50%;
|
||
background: var(--teal-600);
|
||
color: var(--white);
|
||
display: flex; align-items: center; justify-content: center;
|
||
font-weight: 600; font-size: 12px;
|
||
flex-shrink: 0;
|
||
}
|
||
.sidebar-user-info { flex: 1; min-width: 0; }
|
||
.sidebar-user-name {
|
||
font-size: 13px; font-weight: 500;
|
||
white-space: nowrap; overflow: hidden; text-overflow: ellipsis;
|
||
}
|
||
.sidebar-user-role {
|
||
font-size: 11px; color: var(--gray-500);
|
||
}
|
||
|
||
/* -------- Верхняя панель -------- */
|
||
.topbar {
|
||
grid-area: topbar;
|
||
background: var(--white);
|
||
border-bottom: 1px solid var(--gray-200);
|
||
display: flex;
|
||
align-items: center;
|
||
gap: 16px;
|
||
padding: 0 24px;
|
||
position: sticky;
|
||
top: 0;
|
||
z-index: 10;
|
||
}
|
||
.topbar-search {
|
||
flex: 1;
|
||
max-width: 480px;
|
||
position: relative;
|
||
}
|
||
.topbar-search-input {
|
||
width: 100%;
|
||
height: 36px;
|
||
padding: 0 12px 0 38px;
|
||
border: 1px solid var(--gray-200);
|
||
border-radius: var(--radius-md);
|
||
background: var(--gray-50);
|
||
font-size: 13px;
|
||
transition: border-color .15s ease, background-color .15s ease;
|
||
}
|
||
.topbar-search-input::placeholder { color: var(--gray-500); }
|
||
.topbar-search-input:hover { background: var(--white); border-color: var(--gray-300); }
|
||
.topbar-search-input:focus {
|
||
outline: none;
|
||
background: var(--white);
|
||
border-color: var(--teal-600);
|
||
box-shadow: 0 0 0 3px rgba(15, 110, 86, 0.1);
|
||
}
|
||
.topbar-search svg {
|
||
position: absolute;
|
||
left: 12px; top: 50%; transform: translateY(-50%);
|
||
color: var(--gray-500);
|
||
pointer-events: none;
|
||
}
|
||
|
||
.topbar-actions {
|
||
display: flex; align-items: center; gap: 12px;
|
||
margin-left: auto;
|
||
}
|
||
|
||
/* Виджет баланса в шапке (narrative §1.6, §21) */
|
||
.balance-widget {
|
||
display: flex;
|
||
align-items: stretch;
|
||
background: var(--teal-50);
|
||
border: 1px solid var(--teal-100);
|
||
border-radius: var(--radius-md);
|
||
overflow: hidden;
|
||
}
|
||
.balance-cell {
|
||
padding: 6px 14px;
|
||
display: flex;
|
||
flex-direction: column;
|
||
gap: 1px;
|
||
cursor: pointer;
|
||
transition: background-color .15s ease;
|
||
}
|
||
.balance-cell:hover { background: var(--teal-100); }
|
||
.balance-cell + .balance-cell { border-left: 1px solid var(--teal-100); }
|
||
.balance-cell-label {
|
||
font-size: 10px;
|
||
color: var(--teal-900);
|
||
opacity: 0.7;
|
||
letter-spacing: 0.04em;
|
||
text-transform: uppercase;
|
||
font-weight: 600;
|
||
}
|
||
.balance-cell-value {
|
||
font-size: 14px;
|
||
font-weight: 600;
|
||
color: var(--teal-900);
|
||
font-variant-numeric: tabular-nums;
|
||
}
|
||
.balance-cell.is-low .balance-cell-value { color: var(--danger); }
|
||
|
||
.icon-btn {
|
||
width: 36px; height: 36px;
|
||
border: 0;
|
||
background: transparent;
|
||
border-radius: var(--radius-md);
|
||
color: var(--gray-700);
|
||
display: flex; align-items: center; justify-content: center;
|
||
transition: background-color .15s ease, color .15s ease;
|
||
position: relative;
|
||
}
|
||
.icon-btn:hover { background: var(--gray-100); color: var(--gray-900); }
|
||
.icon-btn .dot {
|
||
position: absolute;
|
||
top: 8px; right: 8px;
|
||
width: 8px; height: 8px;
|
||
background: var(--danger);
|
||
border: 2px solid var(--white);
|
||
border-radius: 50%;
|
||
}
|
||
|
||
.topbar-user-trigger {
|
||
display: flex; align-items: center; gap: 8px;
|
||
padding: 4px 10px 4px 4px;
|
||
border: 0;
|
||
background: transparent;
|
||
border-radius: var(--radius-pill);
|
||
transition: background-color .15s ease;
|
||
}
|
||
.topbar-user-trigger:hover { background: var(--gray-100); }
|
||
.topbar-user-trigger .name {
|
||
font-size: 13px; font-weight: 500;
|
||
}
|
||
|
||
/* -------- Главная область -------- */
|
||
.main {
|
||
grid-area: main;
|
||
padding: 24px 32px 48px;
|
||
max-width: 1440px;
|
||
width: 100%;
|
||
}
|
||
|
||
/* Заголовок страницы + контролы */
|
||
.page-head {
|
||
display: flex; align-items: center; justify-content: space-between;
|
||
gap: 16px; flex-wrap: wrap;
|
||
margin-bottom: 8px;
|
||
}
|
||
.page-title {
|
||
font-size: 28px;
|
||
font-weight: 700;
|
||
letter-spacing: -0.02em;
|
||
margin: 0;
|
||
}
|
||
.page-subtitle {
|
||
font-size: 14px;
|
||
color: var(--gray-500);
|
||
margin-bottom: 24px;
|
||
}
|
||
|
||
.controls {
|
||
display: flex; align-items: center; gap: 8px;
|
||
}
|
||
.date-range {
|
||
display: inline-flex;
|
||
align-items: center;
|
||
gap: 8px;
|
||
padding: 8px 14px;
|
||
background: var(--white);
|
||
border: 1px solid var(--gray-200);
|
||
border-radius: var(--radius-md);
|
||
font-size: 13px;
|
||
font-weight: 500;
|
||
color: var(--gray-700);
|
||
transition: border-color .15s ease;
|
||
}
|
||
.date-range:hover { border-color: var(--gray-300); }
|
||
.date-range select {
|
||
border: 0; background: transparent; outline: none;
|
||
font: inherit; color: inherit; padding: 0;
|
||
cursor: pointer;
|
||
}
|
||
.btn-icon-text {
|
||
display: inline-flex;
|
||
align-items: center;
|
||
gap: 6px;
|
||
padding: 8px 14px;
|
||
background: var(--white);
|
||
border: 1px solid var(--gray-200);
|
||
border-radius: var(--radius-md);
|
||
color: var(--gray-700);
|
||
font-size: 13px;
|
||
font-weight: 500;
|
||
transition: border-color .15s ease, color .15s ease;
|
||
}
|
||
.btn-icon-text:hover { border-color: var(--gray-300); color: var(--gray-900); }
|
||
|
||
/* Алерт (низкий баланс) */
|
||
.alert-bar {
|
||
display: flex;
|
||
align-items: flex-start;
|
||
gap: 12px;
|
||
padding: 12px 16px;
|
||
background: #FEF3C7;
|
||
border: 1px solid #FDE68A;
|
||
border-radius: var(--radius-md);
|
||
margin-bottom: 24px;
|
||
color: #78350F;
|
||
font-size: 13px;
|
||
}
|
||
.alert-bar svg { flex-shrink: 0; margin-top: 1px; color: var(--warning); }
|
||
.alert-bar strong { color: #78350F; }
|
||
.alert-bar .alert-action {
|
||
margin-left: auto;
|
||
padding: 6px 14px;
|
||
background: var(--warning);
|
||
color: var(--white);
|
||
border: 0;
|
||
border-radius: var(--radius-md);
|
||
font-size: 12px;
|
||
font-weight: 500;
|
||
}
|
||
.alert-bar .alert-action:hover { background: #D97706; }
|
||
|
||
/* KPI Grid */
|
||
.kpi-grid {
|
||
display: grid;
|
||
grid-template-columns: repeat(5, 1fr);
|
||
gap: 16px;
|
||
margin-bottom: 24px;
|
||
}
|
||
@media (max-width: 1280px) { .kpi-grid { grid-template-columns: repeat(3, 1fr); } }
|
||
@media (max-width: 768px) { .kpi-grid { grid-template-columns: repeat(2, 1fr); } }
|
||
|
||
.kpi-card {
|
||
background: var(--white);
|
||
border: 1px solid var(--gray-200);
|
||
border-radius: var(--radius-lg);
|
||
padding: 20px;
|
||
transition: border-color .15s ease, transform .05s ease;
|
||
}
|
||
.kpi-card:hover { border-color: var(--gray-300); }
|
||
.kpi-card-head {
|
||
display: flex; align-items: center; justify-content: space-between;
|
||
gap: 8px;
|
||
margin-bottom: 12px;
|
||
}
|
||
.kpi-card-label {
|
||
font-size: 12px;
|
||
color: var(--gray-500);
|
||
font-weight: 500;
|
||
}
|
||
.kpi-card-icon {
|
||
width: 28px; height: 28px;
|
||
border-radius: var(--radius-md);
|
||
background: var(--teal-50);
|
||
color: var(--teal-600);
|
||
display: flex; align-items: center; justify-content: center;
|
||
}
|
||
.kpi-card-value {
|
||
font-size: 28px;
|
||
font-weight: 700;
|
||
letter-spacing: -0.02em;
|
||
font-variant-numeric: tabular-nums;
|
||
line-height: 1.1;
|
||
margin-bottom: 4px;
|
||
}
|
||
.kpi-card-trend {
|
||
display: inline-flex;
|
||
align-items: center;
|
||
gap: 4px;
|
||
font-size: 12px;
|
||
font-weight: 500;
|
||
padding: 2px 8px;
|
||
border-radius: var(--radius-pill);
|
||
font-variant-numeric: tabular-nums;
|
||
}
|
||
.kpi-card-trend.is-up { background: #DCFCE7; color: #166534; }
|
||
.kpi-card-trend.is-down { background: #FEE2E2; color: #991B1B; }
|
||
.kpi-card-trend.is-flat { background: var(--gray-100); color: var(--gray-700); }
|
||
|
||
/* Двухколоночный grid под графики и таблицу */
|
||
.grid-2 {
|
||
display: grid;
|
||
grid-template-columns: 2fr 1fr;
|
||
gap: 16px;
|
||
margin-bottom: 24px;
|
||
}
|
||
@media (max-width: 1100px) { .grid-2 { grid-template-columns: 1fr; } }
|
||
|
||
.card {
|
||
background: var(--white);
|
||
border: 1px solid var(--gray-200);
|
||
border-radius: var(--radius-lg);
|
||
padding: 20px;
|
||
}
|
||
.card-head {
|
||
display: flex; align-items: center; justify-content: space-between;
|
||
gap: 8px;
|
||
margin-bottom: 16px;
|
||
}
|
||
.card-title {
|
||
font-size: 16px;
|
||
font-weight: 600;
|
||
letter-spacing: -0.01em;
|
||
margin: 0;
|
||
}
|
||
.card-meta {
|
||
font-size: 12px;
|
||
color: var(--gray-500);
|
||
}
|
||
|
||
/* График активности */
|
||
#chart-activity {
|
||
height: 260px;
|
||
}
|
||
|
||
/* Donut статусов */
|
||
#chart-statuses { height: 280px; }
|
||
|
||
/* Список последних сделок */
|
||
.deals-list { display: flex; flex-direction: column; }
|
||
.deal-row {
|
||
display: grid;
|
||
grid-template-columns: auto 1fr auto auto;
|
||
gap: 12px;
|
||
align-items: center;
|
||
padding: 12px 0;
|
||
border-bottom: 1px solid var(--gray-100);
|
||
transition: background-color .15s ease;
|
||
margin: 0 -12px;
|
||
padding-left: 12px;
|
||
padding-right: 12px;
|
||
border-radius: var(--radius-md);
|
||
cursor: pointer;
|
||
}
|
||
.deal-row:last-child { border-bottom: 0; }
|
||
.deal-row:hover { background: var(--gray-50); }
|
||
.deal-id {
|
||
font-family: var(--font-mono);
|
||
font-size: 11px;
|
||
color: var(--gray-500);
|
||
font-weight: 500;
|
||
}
|
||
.deal-info { min-width: 0; }
|
||
.deal-name {
|
||
font-size: 13px;
|
||
font-weight: 500;
|
||
color: var(--gray-900);
|
||
margin-bottom: 2px;
|
||
white-space: nowrap; overflow: hidden; text-overflow: ellipsis;
|
||
}
|
||
.deal-meta {
|
||
font-size: 12px;
|
||
color: var(--gray-500);
|
||
display: flex; gap: 8px; align-items: center;
|
||
}
|
||
.deal-meta-dot { color: var(--gray-300); }
|
||
.deal-time {
|
||
font-size: 11px;
|
||
color: var(--gray-500);
|
||
white-space: nowrap;
|
||
}
|
||
|
||
/* Бейдж статуса */
|
||
.status-chip {
|
||
display: inline-flex;
|
||
align-items: center;
|
||
gap: 6px;
|
||
padding: 3px 10px;
|
||
border-radius: var(--radius-pill);
|
||
font-size: 11px;
|
||
font-weight: 500;
|
||
white-space: nowrap;
|
||
background: var(--gray-100);
|
||
color: var(--gray-700);
|
||
}
|
||
.status-chip::before {
|
||
content: ''; width: 6px; height: 6px; border-radius: 50%;
|
||
background: currentColor;
|
||
}
|
||
.status-chip.s-new { background: #DBEAFE; color: #1E40AF; }
|
||
.status-chip.s-viewed { background: #EDE9FE; color: #5B21B6; }
|
||
.status-chip.s-worked { background: #CFFAFE; color: #155E75; }
|
||
.status-chip.s-base { background: var(--gray-100); color: var(--gray-700); }
|
||
.status-chip.s-missed { background: #FEF3C7; color: #92400E; }
|
||
.status-chip.s-negotiations { background: #FEF9C3; color: #854D0E; }
|
||
.status-chip.s-waiting_payment { background: #F3E8FF; color: #6B21A8; }
|
||
.status-chip.s-partnership { background: #FCE7F3; color: #9D174D; }
|
||
.status-chip.s-paid { background: #DCFCE7; color: #166534; }
|
||
.status-chip.s-closed { background: var(--gray-100); color: var(--gray-700); }
|
||
.status-chip.s-test_drive { background: #CCFBF1; color: #115E59; }
|
||
.status-chip.s-hot { background: #FEE2E2; color: #991B1B; }
|
||
.status-chip.s-replacement { background: #FFEDD5; color: #9A3412; }
|
||
.status-chip.s-final_missed { background: var(--gray-100); color: var(--gray-900); }
|
||
|
||
/* Виджет напоминаний */
|
||
.reminders-list { display: flex; flex-direction: column; gap: 8px; }
|
||
.reminder {
|
||
display: flex;
|
||
align-items: flex-start;
|
||
gap: 12px;
|
||
padding: 12px;
|
||
border: 1px solid var(--gray-200);
|
||
border-radius: var(--radius-md);
|
||
transition: border-color .15s ease;
|
||
}
|
||
.reminder:hover { border-color: var(--teal-200); }
|
||
.reminder.is-overdue {
|
||
border-color: #FECACA;
|
||
background: #FEF2F2;
|
||
}
|
||
.reminder-time {
|
||
font-family: var(--font-mono);
|
||
font-size: 11px;
|
||
font-weight: 600;
|
||
color: var(--gray-700);
|
||
flex-shrink: 0;
|
||
min-width: 44px;
|
||
}
|
||
.reminder.is-overdue .reminder-time { color: var(--danger); }
|
||
.reminder-body { flex: 1; min-width: 0; }
|
||
.reminder-text {
|
||
font-size: 13px;
|
||
color: var(--gray-900);
|
||
margin-bottom: 2px;
|
||
}
|
||
.reminder-meta {
|
||
font-size: 11px;
|
||
color: var(--gray-500);
|
||
}
|
||
|
||
/* Hint без напоминаний */
|
||
.empty-state {
|
||
text-align: center;
|
||
padding: 24px 16px;
|
||
color: var(--gray-500);
|
||
font-size: 13px;
|
||
}
|
||
|
||
/* Spec dialog (общий с прототипом 01) */
|
||
.spec-toggle {
|
||
position: fixed;
|
||
right: 16px;
|
||
bottom: 16px;
|
||
z-index: 100;
|
||
width: 48px;
|
||
height: 48px;
|
||
border-radius: 50%;
|
||
background: var(--gray-900);
|
||
color: var(--white);
|
||
border: 0;
|
||
box-shadow: var(--shadow-lg);
|
||
font-size: 18px;
|
||
font-weight: 600;
|
||
}
|
||
.spec-toggle:hover { background: var(--teal-600); }
|
||
.spec-overlay {
|
||
position: fixed;
|
||
inset: 0;
|
||
background: rgba(4, 52, 44, 0.6);
|
||
backdrop-filter: blur(2px);
|
||
display: none;
|
||
align-items: center;
|
||
justify-content: center;
|
||
padding: 24px;
|
||
z-index: 99;
|
||
}
|
||
.spec-overlay.is-open { display: flex; }
|
||
.spec-card {
|
||
max-width: 720px;
|
||
width: 100%;
|
||
max-height: 80vh;
|
||
overflow-y: auto;
|
||
background: var(--white);
|
||
border-radius: var(--radius-lg);
|
||
padding: 32px;
|
||
box-shadow: var(--shadow-lg);
|
||
}
|
||
.spec-card h2 {
|
||
margin: 0 0 16px;
|
||
font-size: 20px;
|
||
font-weight: 600;
|
||
}
|
||
.spec-card h3 {
|
||
margin: 24px 0 12px;
|
||
font-size: 13px;
|
||
font-weight: 600;
|
||
color: var(--teal-600);
|
||
text-transform: uppercase;
|
||
letter-spacing: 0.04em;
|
||
}
|
||
.spec-card ul { margin: 0 0 16px; padding-left: 24px; }
|
||
.spec-card li { margin-bottom: 8px; font-size: 13px; line-height: 1.6; color: var(--gray-700); }
|
||
.spec-card code {
|
||
font-family: var(--font-mono);
|
||
font-size: 12px;
|
||
background: var(--gray-100);
|
||
padding: 2px 6px;
|
||
border-radius: var(--radius-sm);
|
||
color: var(--teal-900);
|
||
}
|
||
.btn-secondary {
|
||
background: var(--white);
|
||
color: var(--gray-700);
|
||
border: 1px solid var(--gray-200);
|
||
padding: 8px 16px;
|
||
border-radius: var(--radius-md);
|
||
font-size: 13px;
|
||
font-weight: 500;
|
||
transition: border-color .15s ease;
|
||
}
|
||
.btn-secondary:hover { border-color: var(--gray-300); color: var(--gray-900); }
|
||
|
||
/* Адаптация мобильная — скрываем sidebar (упрощение для прототипа) */
|
||
@media (max-width: 768px) {
|
||
.app {
|
||
grid-template-columns: 1fr;
|
||
grid-template-areas:
|
||
"topbar"
|
||
"main";
|
||
}
|
||
.sidebar { display: none; }
|
||
.main { padding: 16px; }
|
||
.balance-widget { display: none; }
|
||
.topbar-search { max-width: none; }
|
||
}
|
||
</style>
|
||
</head>
|
||
<body>
|
||
|
||
<div class="app">
|
||
|
||
<!-- ===== Боковое меню ===== -->
|
||
<aside class="sidebar">
|
||
<div class="sidebar-brand">
|
||
<div class="brand-mark-circles" aria-hidden="true">
|
||
<span></span><span></span><span></span>
|
||
</div>
|
||
<span class="brand-name">Лидпоток</span>
|
||
</div>
|
||
|
||
<nav class="sidebar-nav" aria-label="Главное меню">
|
||
<div class="sidebar-section">Работа</div>
|
||
|
||
<a href="#" class="sidebar-link is-active">
|
||
<svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"
|
||
stroke-linecap="round" stroke-linejoin="round">
|
||
<rect x="3" y="3" width="7" height="9"/><rect x="14" y="3" width="7" height="5"/>
|
||
<rect x="14" y="12" width="7" height="9"/><rect x="3" y="16" width="7" height="5"/>
|
||
</svg>
|
||
Дашборд
|
||
</a>
|
||
|
||
<a href="#" class="sidebar-link">
|
||
<svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"
|
||
stroke-linecap="round" stroke-linejoin="round">
|
||
<path d="M3 6h18"/><path d="M3 12h18"/><path d="M3 18h18"/>
|
||
</svg>
|
||
Сделки
|
||
<span class="badge">12 899</span>
|
||
</a>
|
||
|
||
<a href="#" class="sidebar-link">
|
||
<svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"
|
||
stroke-linecap="round" stroke-linejoin="round">
|
||
<rect x="3" y="3" width="6" height="18"/><rect x="11" y="3" width="6" height="12"/>
|
||
<rect x="19" y="3" width="2" height="6"/>
|
||
</svg>
|
||
Канбан
|
||
</a>
|
||
|
||
<a href="#" class="sidebar-link">
|
||
<svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"
|
||
stroke-linecap="round" stroke-linejoin="round">
|
||
<path d="M21 16V8a2 2 0 0 0-1-1.73l-7-4a2 2 0 0 0-2 0l-7 4A2 2 0 0 0 3 8v8a2 2 0 0 0 1 1.73l7 4a2 2 0 0 0 2 0l7-4A2 2 0 0 0 21 16z"/>
|
||
</svg>
|
||
Проекты
|
||
<span class="badge">7</span>
|
||
</a>
|
||
|
||
<a href="#" class="sidebar-link">
|
||
<svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"
|
||
stroke-linecap="round" stroke-linejoin="round">
|
||
<line x1="18" y1="20" x2="18" y2="10"/><line x1="12" y1="20" x2="12" y2="4"/>
|
||
<line x1="6" y1="20" x2="6" y2="14"/>
|
||
</svg>
|
||
Аналитика
|
||
</a>
|
||
|
||
<div class="sidebar-section">Управление</div>
|
||
|
||
<a href="#" class="sidebar-link">
|
||
<svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"
|
||
stroke-linecap="round" stroke-linejoin="round">
|
||
<rect x="2" y="5" width="20" height="14" rx="2"/><line x1="2" y1="10" x2="22" y2="10"/>
|
||
</svg>
|
||
Биллинг
|
||
</a>
|
||
|
||
<a href="#" class="sidebar-link">
|
||
<svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"
|
||
stroke-linecap="round" stroke-linejoin="round">
|
||
<circle cx="12" cy="12" r="3"/>
|
||
<path d="M19.4 15a1.65 1.65 0 0 0 .33 1.82l.06.06a2 2 0 0 1 0 2.83 2 2 0 0 1-2.83 0l-.06-.06a1.65 1.65 0 0 0-1.82-.33 1.65 1.65 0 0 0-1 1.51V21a2 2 0 0 1-4 0v-.09A1.65 1.65 0 0 0 9 19.4a1.65 1.65 0 0 0-1.82.33l-.06.06a2 2 0 0 1-2.83 0 2 2 0 0 1 0-2.83l.06-.06a1.65 1.65 0 0 0 .33-1.82 1.65 1.65 0 0 0-1.51-1H3a2 2 0 0 1 0-4h.09A1.65 1.65 0 0 0 4.6 9a1.65 1.65 0 0 0-.33-1.82l-.06-.06a2 2 0 0 1 0-2.83 2 2 0 0 1 2.83 0l.06.06a1.65 1.65 0 0 0 1.82.33H9a1.65 1.65 0 0 0 1-1.51V3a2 2 0 0 1 4 0v.09a1.65 1.65 0 0 0 1 1.51 1.65 1.65 0 0 0 1.82-.33l.06-.06a2 2 0 0 1 2.83 0 2 2 0 0 1 0 2.83l-.06.06a1.65 1.65 0 0 0-.33 1.82V9a1.65 1.65 0 0 0 1.51 1H21a2 2 0 0 1 0 4h-.09a1.65 1.65 0 0 0-1.51 1z"/>
|
||
</svg>
|
||
Настройки
|
||
</a>
|
||
</nav>
|
||
|
||
<div class="sidebar-foot">
|
||
<div class="sidebar-user">
|
||
<div class="avatar">МП</div>
|
||
<div class="sidebar-user-info">
|
||
<div class="sidebar-user-name">Михаил П.</div>
|
||
<div class="sidebar-user-role">Тариф «Профессиональный»</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</aside>
|
||
|
||
<!-- ===== Верхняя панель ===== -->
|
||
<header class="topbar">
|
||
<div class="topbar-search">
|
||
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"
|
||
stroke-linecap="round" stroke-linejoin="round" aria-hidden="true">
|
||
<circle cx="11" cy="11" r="8"/><line x1="21" y1="21" x2="16.65" y2="16.65"/>
|
||
</svg>
|
||
<input type="search" class="topbar-search-input"
|
||
placeholder="Поиск по сделкам, проектам, телефонам…"
|
||
aria-label="Поиск" />
|
||
</div>
|
||
|
||
<div class="topbar-actions">
|
||
<!-- Виджет баланса (narrative §1.6) -->
|
||
<div class="balance-widget" role="status" aria-label="Баланс">
|
||
<button class="balance-cell" type="button" title="Пополнить рублёвый баланс">
|
||
<span class="balance-cell-label">Баланс</span>
|
||
<span class="balance-cell-value">12 450 ₽</span>
|
||
</button>
|
||
<button class="balance-cell is-low" type="button" title="Пополнить лиды">
|
||
<span class="balance-cell-label">Лиды</span>
|
||
<span class="balance-cell-value">8</span>
|
||
</button>
|
||
</div>
|
||
|
||
<button class="icon-btn" type="button" aria-label="Уведомления" title="Уведомления">
|
||
<svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"
|
||
stroke-linecap="round" stroke-linejoin="round">
|
||
<path d="M18 8A6 6 0 0 0 6 8c0 7-3 9-3 9h18s-3-2-3-9"/>
|
||
<path d="M13.73 21a2 2 0 0 1-3.46 0"/>
|
||
</svg>
|
||
<span class="dot"></span>
|
||
</button>
|
||
|
||
<button class="topbar-user-trigger" type="button" aria-label="Меню пользователя">
|
||
<div class="avatar">МП</div>
|
||
<span class="name">Михаил</span>
|
||
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"
|
||
stroke-linecap="round" stroke-linejoin="round" aria-hidden="true">
|
||
<polyline points="6 9 12 15 18 9"/>
|
||
</svg>
|
||
</button>
|
||
</div>
|
||
</header>
|
||
|
||
<!-- ===== Основное содержимое ===== -->
|
||
<main class="main">
|
||
|
||
<!-- Заголовок страницы + контролы периода -->
|
||
<div class="page-head">
|
||
<div>
|
||
<h1 class="page-title">Дашборд</h1>
|
||
<div class="page-subtitle">Сводка работы за выбранный период · обновлено 2 минуты назад</div>
|
||
</div>
|
||
<div class="controls">
|
||
<div class="date-range">
|
||
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"
|
||
stroke-linecap="round" stroke-linejoin="round">
|
||
<rect x="3" y="4" width="18" height="18" rx="2"/>
|
||
<line x1="16" y1="2" x2="16" y2="6"/><line x1="8" y1="2" x2="8" y2="6"/>
|
||
<line x1="3" y1="10" x2="21" y2="10"/>
|
||
</svg>
|
||
<select id="period-select" aria-label="Период">
|
||
<option value="today">Сегодня</option>
|
||
<option value="7d" selected>Последние 7 дней</option>
|
||
<option value="30d">Последние 30 дней</option>
|
||
<option value="month">Текущий месяц</option>
|
||
<option value="custom">Выбрать диапазон…</option>
|
||
</select>
|
||
</div>
|
||
<button class="btn-icon-text" type="button" title="Скачать отчёт за период">
|
||
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"
|
||
stroke-linecap="round" stroke-linejoin="round">
|
||
<path d="M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4"/>
|
||
<polyline points="7 10 12 15 17 10"/><line x1="12" y1="15" x2="12" y2="3"/>
|
||
</svg>
|
||
Экспорт
|
||
</button>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- Алерт «Низкий баланс» (narrative §21.3, ДЕФ low_balance_threshold = 10) -->
|
||
<div class="alert-bar" role="alert">
|
||
<svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"
|
||
stroke-linecap="round" stroke-linejoin="round" aria-hidden="true">
|
||
<path d="M10.29 3.86 1.82 18a2 2 0 0 0 1.71 3h16.94a2 2 0 0 0 1.71-3L13.71 3.86a2 2 0 0 0-3.42 0z"/>
|
||
<line x1="12" y1="9" x2="12" y2="13"/><line x1="12" y1="17" x2="12.01" y2="17"/>
|
||
</svg>
|
||
<span>
|
||
<strong>Лидов осталось 8</strong> — это ниже порога 10.
|
||
Рекомендуем пополнить баланс, чтобы не пропустить входящие лиды.
|
||
</span>
|
||
<button class="alert-action" type="button">Пополнить</button>
|
||
</div>
|
||
|
||
<!-- ===== KPI-карточки (narrative §12.2) ===== -->
|
||
<section class="kpi-grid" aria-label="Ключевые показатели">
|
||
|
||
<!-- 1. Лимит активных проектов -->
|
||
<article class="kpi-card">
|
||
<div class="kpi-card-head">
|
||
<span class="kpi-card-label">Лимит проектов</span>
|
||
<span class="kpi-card-icon" aria-hidden="true">
|
||
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"
|
||
stroke-linecap="round" stroke-linejoin="round">
|
||
<path d="M21 16V8a2 2 0 0 0-1-1.73l-7-4a2 2 0 0 0-2 0l-7 4A2 2 0 0 0 3 8v8a2 2 0 0 0 1 1.73l7 4a2 2 0 0 0 2 0l7-4A2 2 0 0 0 21 16z"/>
|
||
</svg>
|
||
</span>
|
||
</div>
|
||
<div class="kpi-card-value">450</div>
|
||
<div class="kpi-card-trend is-flat">7 активных проектов</div>
|
||
</article>
|
||
|
||
<!-- 2. Получено за период -->
|
||
<article class="kpi-card">
|
||
<div class="kpi-card-head">
|
||
<span class="kpi-card-label">Получено лидов</span>
|
||
<span class="kpi-card-icon" aria-hidden="true">
|
||
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"
|
||
stroke-linecap="round" stroke-linejoin="round">
|
||
<polyline points="23 6 13.5 15.5 8.5 10.5 1 18"/><polyline points="17 6 23 6 23 12"/>
|
||
</svg>
|
||
</span>
|
||
</div>
|
||
<div class="kpi-card-value">487</div>
|
||
<div class="kpi-card-trend is-up">
|
||
<svg width="10" height="10" viewBox="0 0 24 24" fill="currentColor" aria-hidden="true">
|
||
<path d="M7 14l5-5 5 5z"/>
|
||
</svg>
|
||
+18.4% к прошлой неделе
|
||
</div>
|
||
</article>
|
||
|
||
<!-- 3. Конверсия -->
|
||
<article class="kpi-card">
|
||
<div class="kpi-card-head">
|
||
<span class="kpi-card-label">Конверсия в оплачено</span>
|
||
<span class="kpi-card-icon" aria-hidden="true">
|
||
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"
|
||
stroke-linecap="round" stroke-linejoin="round">
|
||
<path d="M22 11.08V12a10 10 0 1 1-5.93-9.14"/><polyline points="22 4 12 14.01 9 11.01"/>
|
||
</svg>
|
||
</span>
|
||
</div>
|
||
<div class="kpi-card-value">14.2%</div>
|
||
<div class="kpi-card-trend is-down">
|
||
<svg width="10" height="10" viewBox="0 0 24 24" fill="currentColor" aria-hidden="true">
|
||
<path d="M7 10l5 5 5-5z"/>
|
||
</svg>
|
||
−2.1 п.п. к прошлой неделе
|
||
</div>
|
||
</article>
|
||
|
||
<!-- 4. Звонков (Post-MVP, показываем заглушку) -->
|
||
<article class="kpi-card" style="opacity: 0.6;">
|
||
<div class="kpi-card-head">
|
||
<span class="kpi-card-label">Звонков</span>
|
||
<span class="kpi-card-icon" aria-hidden="true">
|
||
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"
|
||
stroke-linecap="round" stroke-linejoin="round">
|
||
<path d="M22 16.92v3a2 2 0 0 1-2.18 2 19.79 19.79 0 0 1-8.63-3.07 19.5 19.5 0 0 1-6-6 19.79 19.79 0 0 1-3.07-8.67A2 2 0 0 1 4.11 2h3a2 2 0 0 1 2 1.72 12.84 12.84 0 0 0 .7 2.81 2 2 0 0 1-.45 2.11L8.09 9.91a16 16 0 0 0 6 6l1.27-1.27a2 2 0 0 1 2.11-.45 12.84 12.84 0 0 0 2.81.7A2 2 0 0 1 22 16.92z"/>
|
||
</svg>
|
||
</span>
|
||
</div>
|
||
<div class="kpi-card-value">—</div>
|
||
<div class="kpi-card-trend is-flat">Доступно с интеграцией телефонии</div>
|
||
</article>
|
||
|
||
<!-- 5. Активных напоминаний -->
|
||
<article class="kpi-card">
|
||
<div class="kpi-card-head">
|
||
<span class="kpi-card-label">Активные напоминания</span>
|
||
<span class="kpi-card-icon" aria-hidden="true">
|
||
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"
|
||
stroke-linecap="round" stroke-linejoin="round">
|
||
<circle cx="12" cy="13" r="8"/><polyline points="12 9 12 13 14.5 14.5"/>
|
||
<path d="M5 3 2 6"/><path d="m22 6-3-3"/>
|
||
</svg>
|
||
</span>
|
||
</div>
|
||
<div class="kpi-card-value">14</div>
|
||
<div class="kpi-card-trend is-flat">3 просрочены, 5 на сегодня</div>
|
||
</article>
|
||
</section>
|
||
|
||
<!-- ===== График активности + Donut статусов ===== -->
|
||
<section class="grid-2">
|
||
|
||
<!-- График активности -->
|
||
<article class="card">
|
||
<header class="card-head">
|
||
<h2 class="card-title">Получено лидов по дням</h2>
|
||
<span class="card-meta">Последние 7 дней</span>
|
||
</header>
|
||
<div id="chart-activity" role="img" aria-label="График количества лидов по дням за последние 7 дней"></div>
|
||
</article>
|
||
|
||
<!-- Donut по статусам -->
|
||
<article class="card">
|
||
<header class="card-head">
|
||
<h2 class="card-title">Распределение по статусам</h2>
|
||
<span class="card-meta">487 сделок</span>
|
||
</header>
|
||
<div id="chart-statuses" role="img" aria-label="Круговая диаграмма распределения сделок по статусам"></div>
|
||
</article>
|
||
</section>
|
||
|
||
<!-- ===== Последние сделки + Напоминания ===== -->
|
||
<section class="grid-2">
|
||
|
||
<!-- Последние сделки -->
|
||
<article class="card">
|
||
<header class="card-head">
|
||
<h2 class="card-title">Последние сделки</h2>
|
||
<a href="#" class="card-meta">Все сделки →</a>
|
||
</header>
|
||
|
||
<div class="deals-list" id="deals-list">
|
||
<!-- Заполняется JS-ом ниже -->
|
||
</div>
|
||
</article>
|
||
|
||
<!-- Напоминания -->
|
||
<article class="card">
|
||
<header class="card-head">
|
||
<h2 class="card-title">Ближайшие напоминания</h2>
|
||
<a href="#" class="card-meta">Все →</a>
|
||
</header>
|
||
|
||
<div class="reminders-list" id="reminders-list">
|
||
<!-- Заполняется JS-ом ниже -->
|
||
</div>
|
||
</article>
|
||
</section>
|
||
|
||
</main>
|
||
</div>
|
||
|
||
<!-- ===== Spec-аннотация ===== -->
|
||
<button class="spec-toggle" id="spec-toggle" aria-label="Открыть спецификацию экрана">i</button>
|
||
<div class="spec-overlay" id="spec-overlay" role="dialog" aria-labelledby="spec-title">
|
||
<div class="spec-card">
|
||
<h2 id="spec-title">Спецификация экрана №2 — Дашборд</h2>
|
||
<p style="margin: 0 0 16px; font-size: 13px; color: var(--gray-500);">
|
||
Прил. Л v0.1, 05.05.2026. Источник истины: <code>brandbook.md</code> v1.1, narrative §12, §8.1, §21.3.
|
||
</p>
|
||
|
||
<h3>Структура экрана</h3>
|
||
<ul>
|
||
<li><strong>Sidebar</strong> — фиксированный, 240 px, навигация по 7 разделам клиентского приложения.</li>
|
||
<li><strong>Topbar</strong> — поиск + виджет баланса (рубли + лиды) + уведомления + меню пользователя.</li>
|
||
<li><strong>Алерт «Низкий баланс»</strong> — показывается при <code>balance_leads ≤ low_balance_threshold</code> (default 10, см. <code>system_settings.low_balance_threshold_leads</code>).</li>
|
||
<li><strong>5 KPI-карточек</strong> — лимит проектов, получено за период, конверсия, звонки (Post-MVP), напоминания.</li>
|
||
<li><strong>2 графика</strong> — линейный по дням, donut по статусам (ApexCharts через CDN).</li>
|
||
<li><strong>2 списка</strong> — последние 5 сделок и ближайшие 4 напоминания.</li>
|
||
</ul>
|
||
|
||
<h3>API контракты (для backend)</h3>
|
||
<ul>
|
||
<li><code>GET /api/v1/dashboard/kpi?period=7d</code> → 5 показателей с динамикой к предыдущему периоду.</li>
|
||
<li><code>GET /api/v1/dashboard/activity?period=7d&granularity=day</code> → ряд для графика.</li>
|
||
<li><code>GET /api/v1/dashboard/statuses?period=7d</code> → распределение по 14 статусам.</li>
|
||
<li><code>GET /api/v1/deals?limit=5&sort=-received_at</code> → последние сделки.</li>
|
||
<li><code>GET /api/v1/reminders?status=upcoming&limit=4</code> → ближайшие напоминания (Биз-10, отдельная таблица <code>reminders</code>).</li>
|
||
<li>Кеш на стороне сервера 5 мин для KPI и графиков (narrative §12).</li>
|
||
</ul>
|
||
|
||
<h3>14 статусов воронки (narrative §8.1)</h3>
|
||
<ul>
|
||
<li>Цвета зафиксированы как CSS-переменные <code>--status-{slug}</code>. Названия в UI — из <code>tenant_status_overrides</code>; системные (6 шт) не переименовываются.</li>
|
||
<li>Бейдж — <code>.status-chip.s-{slug}</code> с пастельным фоном и тёмным текстом из той же палитры (контраст ≥ 4.5:1).</li>
|
||
</ul>
|
||
|
||
<h3>Безопасность и приватность</h3>
|
||
<ul>
|
||
<li>Телефоны в списке последних сделок — <strong>маскированы</strong>: <code>+7 (909) ***-67-89</code> (последние 2 цифры). Полный — только в карточке после клика.</li>
|
||
<li>RLS работает прозрачно: все запросы выполняются под <code>crm_app_user</code> с <code>SET LOCAL app.current_tenant_id</code>.</li>
|
||
<li>Поиск в шапке по умолчанию ищет в пределах тенанта.</li>
|
||
</ul>
|
||
|
||
<h3>Доступность</h3>
|
||
<ul>
|
||
<li>Sidebar помечен <code>aria-label="Главное меню"</code>, активная ссылка — <code>aria-current="page"</code> (TODO в боевом коде).</li>
|
||
<li>Графики имеют <code>role="img"</code> и <code>aria-label</code> с человекочитаемым описанием.</li>
|
||
<li>Всё работает с клавиатуры; visible focus.</li>
|
||
<li>Контраст брендовых сочетаний — см. brandbook §3.4.</li>
|
||
</ul>
|
||
|
||
<h3>Что не реализовано в прототипе</h3>
|
||
<ul>
|
||
<li>Реальный скелетон-loading (для прода — fade-in после fetch).</li>
|
||
<li>WebSocket / SSE для realtime-обновления баланса и счётчиков (Post-MVP).</li>
|
||
<li>Кастомный date range picker (заменён на <code><select></code>).</li>
|
||
<li>Тёмная тема (готова в <code>brandbook §3.3</code>, но Vuetify-тема <code>lidpotokDark</code> подключается на v2).</li>
|
||
<li>Push-уведомления через колокольчик (требуют VAPID, см. <code>narrative §17.4</code>).</li>
|
||
</ul>
|
||
|
||
<button type="button" class="btn-secondary" id="spec-close" style="margin-top: 16px; width: 100%;">Закрыть</button>
|
||
</div>
|
||
</div>
|
||
|
||
<script>
|
||
// =========================================================================
|
||
// Прил. Л — прототип №2: stub-данные и инициализация графиков
|
||
// =========================================================================
|
||
|
||
// ---- Список статусов с цветами (narrative §8.1) ----
|
||
const STATUSES = {
|
||
new: { name: 'Новые', color: '#3B82F6' },
|
||
viewed: { name: 'Просмотрено', color: '#8B5CF6' },
|
||
worked: { name: 'Проработан', color: '#06B6D4' },
|
||
base: { name: 'База', color: '#888780' },
|
||
missed: { name: 'Недозвон', color: '#F59E0B' },
|
||
negotiations: { name: 'Переговоры', color: '#EAB308' },
|
||
waiting_payment: { name: 'Ожидаем оплаты', color: '#C084FC' },
|
||
partnership: { name: 'Партнёрка', color: '#EC4899' },
|
||
paid: { name: 'Оплачено', color: '#22C55E' },
|
||
closed: { name: 'Закрыто', color: '#888780' },
|
||
test_drive: { name: 'Тест-драйв', color: '#14B8A6' },
|
||
hot: { name: 'Горячий', color: '#EF4444' },
|
||
replacement: { name: 'На замену', color: '#F97316' },
|
||
final_missed: { name: 'Конечный недозвон', color: '#444441' },
|
||
};
|
||
|
||
// ---- График активности (линейный, 7 дней) ----
|
||
const activityChart = new ApexCharts(document.getElementById('chart-activity'), {
|
||
chart: {
|
||
type: 'area',
|
||
height: 260,
|
||
toolbar: { show: false },
|
||
animations: { enabled: true, speed: 400 },
|
||
fontFamily: 'Inter, sans-serif',
|
||
},
|
||
series: [{
|
||
name: 'Лиды',
|
||
data: [42, 58, 73, 51, 89, 96, 78],
|
||
}],
|
||
xaxis: {
|
||
categories: ['Чт 30', 'Пт 1', 'Сб 2', 'Вс 3', 'Пн 4', 'Вт 5', 'Ср 6'],
|
||
axisBorder: { show: false },
|
||
axisTicks: { show: false },
|
||
labels: { style: { colors: '#888780', fontSize: '11px', fontWeight: 500 } },
|
||
},
|
||
yaxis: {
|
||
labels: { style: { colors: '#888780', fontSize: '11px' }, formatter: v => Math.round(v) },
|
||
},
|
||
colors: ['#0F6E56'],
|
||
fill: {
|
||
type: 'gradient',
|
||
gradient: {
|
||
shadeIntensity: 1, opacityFrom: 0.35, opacityTo: 0.05, stops: [0, 100],
|
||
},
|
||
},
|
||
stroke: { curve: 'smooth', width: 2.5 },
|
||
dataLabels: { enabled: false },
|
||
grid: { borderColor: '#F1EFE8', strokeDashArray: 4, padding: { left: 0, right: 0 } },
|
||
tooltip: {
|
||
theme: 'light',
|
||
style: { fontSize: '12px' },
|
||
y: { formatter: v => v + ' лидов' },
|
||
},
|
||
markers: { size: 0, hover: { size: 5 } },
|
||
});
|
||
|
||
// ---- Donut статусов ----
|
||
const statusData = [
|
||
{ slug: 'new', count: 124 },
|
||
{ slug: 'viewed', count: 96 },
|
||
{ slug: 'worked', count: 78 },
|
||
{ slug: 'negotiations', count: 64 },
|
||
{ slug: 'waiting_payment', count: 42 },
|
||
{ slug: 'paid', count: 69 },
|
||
{ slug: 'missed', count: 31 },
|
||
{ slug: 'hot', count: 12 },
|
||
{ slug: 'closed', count: 21 },
|
||
{ slug: 'final_missed', count: 18 },
|
||
];
|
||
const statusesChart = new ApexCharts(document.getElementById('chart-statuses'), {
|
||
chart: {
|
||
type: 'donut',
|
||
height: 280,
|
||
fontFamily: 'Inter, sans-serif',
|
||
animations: { enabled: true, speed: 400 },
|
||
},
|
||
series: statusData.map(s => s.count),
|
||
labels: statusData.map(s => STATUSES[s.slug].name),
|
||
colors: statusData.map(s => STATUSES[s.slug].color),
|
||
stroke: { width: 2, colors: ['#FFFFFF'] },
|
||
legend: {
|
||
position: 'bottom',
|
||
fontSize: '11px',
|
||
itemMargin: { horizontal: 6, vertical: 4 },
|
||
markers: { width: 8, height: 8, radius: 8 },
|
||
labels: { colors: '#444441' },
|
||
},
|
||
dataLabels: { enabled: false },
|
||
plotOptions: {
|
||
pie: {
|
||
donut: {
|
||
size: '70%',
|
||
labels: {
|
||
show: true,
|
||
name: { fontSize: '12px', color: '#888780', offsetY: 16 },
|
||
value: {
|
||
fontSize: '24px', fontWeight: 700, color: '#04342C', offsetY: -8,
|
||
formatter: v => v,
|
||
},
|
||
total: {
|
||
show: true, label: 'Всего сделок',
|
||
fontSize: '11px', color: '#888780',
|
||
formatter: w => w.globals.seriesTotals.reduce((a, b) => a + b, 0),
|
||
},
|
||
},
|
||
},
|
||
},
|
||
},
|
||
tooltip: {
|
||
y: { formatter: v => v + ' сделок' },
|
||
},
|
||
});
|
||
|
||
// ---- Запуск графиков после готовности DOM ----
|
||
document.addEventListener('DOMContentLoaded', () => {
|
||
activityChart.render();
|
||
statusesChart.render();
|
||
});
|
||
// Если DOM уже готов — рендерим сразу
|
||
if (document.readyState !== 'loading') {
|
||
activityChart.render();
|
||
statusesChart.render();
|
||
}
|
||
|
||
// ---- Список последних сделок ----
|
||
const deals = [
|
||
{ id: 12416101, name: 'Александр К.', phone: '+7 (909) ***-67-89', project: 'Caranga', tag: 'B3', status: 'hot', time: '5 мин назад' },
|
||
{ id: 12416099, name: 'Мария В.', phone: '+7 (915) ***-12-04', project: 'drivez', tag: 'B1', status: 'negotiations', time: '17 мин назад' },
|
||
{ id: 12416094, name: 'Без имени', phone: '+7 (985) ***-44-56', project: 'carmoney', tag: 'B2', status: 'new', time: '32 мин назад' },
|
||
{ id: 12416088, name: 'Олег Д.', phone: '+7 (903) ***-90-21', project: 'Vashinvest', tag: 'B3', status: 'paid', time: '54 мин назад' },
|
||
{ id: 12416077, name: 'Ирина М.', phone: '+7 (916) ***-08-15', project: 'Caranga', tag: 'B3', status: 'waiting_payment', time: '1 ч назад' },
|
||
];
|
||
const dealsList = document.getElementById('deals-list');
|
||
deals.forEach(d => {
|
||
const row = document.createElement('div');
|
||
row.className = 'deal-row';
|
||
row.innerHTML = `
|
||
<span class="deal-id">#${d.id}</span>
|
||
<div class="deal-info">
|
||
<div class="deal-name">${d.name}</div>
|
||
<div class="deal-meta">
|
||
<span>${d.phone}</span>
|
||
<span class="deal-meta-dot">·</span>
|
||
<span>${d.project}</span>
|
||
<span class="deal-meta-dot">·</span>
|
||
<span>${d.tag}</span>
|
||
</div>
|
||
</div>
|
||
<span class="status-chip s-${d.status}">${STATUSES[d.status].name}</span>
|
||
<span class="deal-time">${d.time}</span>
|
||
`;
|
||
dealsList.appendChild(row);
|
||
});
|
||
|
||
// ---- Виджет напоминаний ----
|
||
const reminders = [
|
||
{ time: 'Сейчас', text: 'Перезвонить Александру К.', meta: 'Сделка #12416101 · Caranga', overdue: true },
|
||
{ time: '14:30', text: 'Отправить КП Марии В.', meta: 'Сделка #12416099 · drivez', overdue: false },
|
||
{ time: '16:00', text: 'Уточнить статус оплаты', meta: 'Сделка #12416077 · Caranga', overdue: false },
|
||
{ time: 'Завтра', text: 'Согласовать тест-драйв', meta: 'Сделка #12415988 · drivez', overdue: false },
|
||
];
|
||
const remindersList = document.getElementById('reminders-list');
|
||
reminders.forEach(r => {
|
||
const item = document.createElement('div');
|
||
item.className = 'reminder' + (r.overdue ? ' is-overdue' : '');
|
||
item.innerHTML = `
|
||
<span class="reminder-time">${r.time}</span>
|
||
<div class="reminder-body">
|
||
<div class="reminder-text">${r.text}</div>
|
||
<div class="reminder-meta">${r.meta}</div>
|
||
</div>
|
||
`;
|
||
remindersList.appendChild(item);
|
||
});
|
||
|
||
// ---- Period select — заглушка ----
|
||
document.getElementById('period-select').addEventListener('change', (e) => {
|
||
if (e.target.value === 'custom') {
|
||
alert('Откроется кастомный date range picker (на боевом — Vuetify v-date-picker в режиме range).');
|
||
e.target.value = '7d';
|
||
}
|
||
});
|
||
|
||
// ---- Spec dialog ----
|
||
const specToggle = document.getElementById('spec-toggle');
|
||
const specOverlay = document.getElementById('spec-overlay');
|
||
const specClose = document.getElementById('spec-close');
|
||
specToggle.addEventListener('click', () => specOverlay.classList.add('is-open'));
|
||
specClose.addEventListener('click', () => specOverlay.classList.remove('is-open'));
|
||
specOverlay.addEventListener('click', (e) => {
|
||
if (e.target === specOverlay) specOverlay.classList.remove('is-open');
|
||
});
|
||
document.addEventListener('keydown', (e) => {
|
||
if (e.key === 'Escape') specOverlay.classList.remove('is-open');
|
||
});
|
||
</script>
|
||
|
||
</body>
|
||
</html>
|