905 lines
40 KiB
HTML
905 lines
40 KiB
HTML
<!DOCTYPE html>
|
||
<html lang="ru">
|
||
<head>
|
||
<meta charset="utf-8">
|
||
<meta name="viewport" content="width=device-width,initial-scale=1">
|
||
<title>Коммунальный бюджет | Корпоративная система</title>
|
||
<link rel="preconnect" href="https://fonts.googleapis.com">
|
||
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700&display=swap" rel="stylesheet">
|
||
<link rel="icon" href="data:image/svg+xml,<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 100 100'><text y='.9em' font-size='90'>📊</text></svg>">
|
||
<style>
|
||
:root {
|
||
--primary: #1e3a5f;
|
||
--primary-light: #2c5282;
|
||
--primary-dark: #0f2744;
|
||
--accent: #3182ce;
|
||
--success: #38a169;
|
||
--danger: #e53e3e;
|
||
--warning: #d69e2e;
|
||
--bg: #f7fafc;
|
||
--white: #ffffff;
|
||
--gray-100: #f7fafc;
|
||
--gray-200: #edf2f7;
|
||
--gray-300: #e2e8f0;
|
||
--gray-400: #cbd5e0;
|
||
--gray-500: #718096;
|
||
--gray-600: #4a5568;
|
||
--gray-700: #2d3748;
|
||
--gray-800: #1a202c;
|
||
--shadow: 0 1px 3px rgba(0,0,0,0.1);
|
||
--shadow-md: 0 4px 6px rgba(0,0,0,0.1);
|
||
--shadow-lg: 0 10px 15px rgba(0,0,0,0.1);
|
||
--radius: 8px;
|
||
--radius-lg: 12px;
|
||
}
|
||
* { box-sizing: border-box; margin: 0; padding: 0; }
|
||
body { font: 14px/1.5 'Inter', -apple-system, BlinkMacSystemFont, sans-serif; color: var(--gray-800); background: var(--bg); }
|
||
a { text-decoration: none; color: inherit; }
|
||
button { cursor: pointer; font-family: inherit; }
|
||
|
||
/* Layout */
|
||
.app { display: flex; min-height: 100vh; }
|
||
.sidebar { width: 260px; background: var(--primary); color: var(--white); position: fixed; inset: 0 0 0 auto; overflow-y: auto; z-index: 100; transition: transform 0.3s; }
|
||
.sidebar-header { padding: 20px; border-bottom: 1px solid rgba(255,255,255,0.1); }
|
||
.sidebar-title { font-size: 18px; font-weight: 700; }
|
||
.sidebar-nav { padding: 20px 0; }
|
||
.nav-item { display: flex; align-items: center; gap: 12px; padding: 12px 20px; color: rgba(255,255,255,0.8); transition: all 0.2s; cursor: pointer; }
|
||
.nav-item:hover, .nav-item.active { background: rgba(255,255,255,0.1); color: var(--white); }
|
||
.nav-icon { font-size: 18px; width: 20px; text-align: center; }
|
||
.main { flex: 1; margin-left: 260px; display: flex; flex-direction: column; }
|
||
|
||
/* Header */
|
||
.header { background: var(--white); border-bottom: 1px solid var(--gray-200); padding: 16px 24px; display: flex; justify-content: space-between; align-items: center; position: sticky; top: 0; z-index: 50; }
|
||
.header-left { display: flex; align-items: center; gap: 16px; }
|
||
.year-select { padding: 8px 16px; border: 1px solid var(--gray-300); border-radius: var(--radius); font-size: 14px; font-weight: 600; background: var(--white); cursor: pointer; }
|
||
.header-right { display: flex; align-items: center; gap: 16px; }
|
||
.notification-btn { position: relative; background: none; border: none; font-size: 20px; color: var(--gray-500); cursor: pointer; }
|
||
.notification-badge { position: absolute; top: -4px; right: -4px; width: 8px; height: 8px; background: var(--danger); border-radius: 50%; }
|
||
.user-menu { display: flex; align-items: center; gap: 8px; font-weight: 600; color: var(--gray-700); }
|
||
.user-avatar { width: 32px; height: 32px; background: var(--accent); border-radius: 50%; display: flex; align-items: center; justify-content: center; color: var(--white); font-weight: 700; }
|
||
.menu-toggle { display: none; background: none; border: none; font-size: 24px; color: var(--gray-600); cursor: pointer; }
|
||
|
||
/* Content */
|
||
.content { padding: 24px; }
|
||
.page { display: none; }
|
||
.page.active { display: block; }
|
||
.page-header { margin-bottom: 24px; }
|
||
.page-title { font-size: 28px; font-weight: 700; color: var(--gray-800); margin-bottom: 8px; }
|
||
.page-subtitle { color: var(--gray-500); }
|
||
|
||
/* KPI Cards */
|
||
.kpi-grid { display: grid; grid-template-columns: repeat(auto-fit, minmax(240px, 1fr)); gap: 16px; margin-bottom: 24px; }
|
||
.kpi-card { background: var(--white); border-radius: var(--radius-lg); padding: 20px; box-shadow: var(--shadow); border-left: 4px solid var(--accent); }
|
||
.kpi-card.success { border-left-color: var(--success); }
|
||
.kpi-card.danger { border-left-color: var(--danger); }
|
||
.kpi-card.warning { border-left-color: var(--warning); }
|
||
.kpi-label { font-size: 13px; color: var(--gray-500); margin-bottom: 8px; text-transform: uppercase; letter-spacing: 0.5px; }
|
||
.kpi-value { font-size: 24px; font-weight: 700; color: var(--gray-800); }
|
||
.kpi-delta { font-size: 13px; margin-top: 8px; display: flex; align-items: center; gap: 4px; }
|
||
.kpi-delta.positive { color: var(--success); }
|
||
.kpi-delta.negative { color: var(--danger); }
|
||
|
||
/* Cards */
|
||
.card { background: var(--white); border-radius: var(--radius-lg); box-shadow: var(--shadow); margin-bottom: 24px; }
|
||
.card-header { padding: 20px; border-bottom: 1px solid var(--gray-200); display: flex; justify-content: space-between; align-items: center; }
|
||
.card-title { font-size: 18px; font-weight: 600; }
|
||
.card-body { padding: 20px; }
|
||
|
||
/* Tables */
|
||
.table-container { overflow-x: auto; }
|
||
table { width: 100%; border-collapse: collapse; }
|
||
th, td { padding: 12px 16px; text-align: left; border-bottom: 1px solid var(--gray-200); }
|
||
th { font-weight: 600; color: var(--gray-600); font-size: 13px; text-transform: uppercase; letter-spacing: 0.5px; background: var(--gray-50); }
|
||
tr:hover { background: var(--gray-50); }
|
||
.status { display: inline-block; padding: 4px 12px; border-radius: 20px; font-size: 12px; font-weight: 600; }
|
||
.status.active { background: #c6f6d5; color: #22543d; }
|
||
.status.archived { background: var(--gray-200); color: var(--gray-600); }
|
||
.status.draft { background: #fefcbf; color: #744210; }
|
||
.status.approved { background: #bee3f8; color: #2c5282; }
|
||
|
||
/* Buttons */
|
||
.btn { display: inline-flex; align-items: center; gap: 8px; padding: 10px 20px; border-radius: var(--radius); font-weight: 600; font-size: 14px; border: none; transition: all 0.2s; }
|
||
.btn-primary { background: var(--accent); color: var(--white); }
|
||
.btn-primary:hover { background: var(--primary-light); }
|
||
.btn-secondary { background: var(--gray-200); color: var(--gray-700); }
|
||
.btn-secondary:hover { background: var(--gray-300); }
|
||
.btn-success { background: var(--success); color: var(--white); }
|
||
.btn-danger { background: var(--danger); color: var(--white); }
|
||
.btn-sm { padding: 6px 12px; font-size: 13px; }
|
||
.btn-icon { background: none; border: none; font-size: 18px; color: var(--gray-500); cursor: pointer; padding: 4px; }
|
||
.btn-icon:hover { color: var(--accent); }
|
||
|
||
/* Forms */
|
||
.form-group { margin-bottom: 16px; }
|
||
.form-label { display: block; font-weight: 600; margin-bottom: 8px; color: var(--gray-700); }
|
||
.form-input, .form-select { width: 100%; padding: 10px 14px; border: 1px solid var(--gray-300); border-radius: var(--radius); font-size: 14px; font-family: inherit; }
|
||
.form-input:focus, .form-select:focus { outline: none; border-color: var(--accent); box-shadow: 0 0 0 3px rgba(49, 130, 206, 0.1); }
|
||
.form-row { display: grid; grid-template-columns: repeat(auto-fit, minmax(200px, 1fr)); gap: 16px; }
|
||
|
||
/* Modal */
|
||
.modal { position: fixed; inset: 0; background: rgba(0,0,0,0.5); z-index: 1000; display: none; align-items: center; justify-content: center; }
|
||
.modal.open { display: flex; }
|
||
.modal-content { background: var(--white); border-radius: var(--radius-lg); max-width: 600px; width: 90%; max-height: 90vh; overflow-y: auto; }
|
||
.modal-header { padding: 20px; border-bottom: 1px solid var(--gray-200); display: flex; justify-content: space-between; align-items: center; }
|
||
.modal-title { font-size: 20px; font-weight: 700; }
|
||
.modal-close { background: none; border: none; font-size: 24px; color: var(--gray-500); cursor: pointer; }
|
||
.modal-body { padding: 20px; }
|
||
.modal-footer { padding: 20px; border-top: 1px solid var(--gray-200); display: flex; justify-content: flex-end; gap: 12px; }
|
||
|
||
/* Alerts */
|
||
.alert { padding: 16px; border-radius: var(--radius); margin-bottom: 16px; display: flex; align-items: start; gap: 12px; }
|
||
.alert-warning { background: #fffaf0; border-left: 4px solid var(--warning); }
|
||
.alert-danger { background: #fff5f5; border-left: 4px solid var(--danger); }
|
||
.alert-icon { font-size: 20px; }
|
||
.alert-content { flex: 1; }
|
||
.alert-title { font-weight: 600; margin-bottom: 4px; }
|
||
.alert-text { color: var(--gray-600); font-size: 14px; }
|
||
|
||
/* Charts placeholder */
|
||
.chart-container { height: 300px; position: relative; }
|
||
.chart-placeholder { display: flex; align-items: center; justify-content: center; height: 100%; color: var(--gray-400); font-size: 14px; }
|
||
|
||
/* Timeline */
|
||
.timeline { position: relative; padding-left: 30px; }
|
||
.timeline::before { content: ''; position: absolute; left: 8px; top: 0; bottom: 0; width: 2px; background: var(--gray-300); }
|
||
.timeline-item { position: relative; padding-bottom: 20px; }
|
||
.timeline-marker { position: absolute; left: -26px; width: 18px; height: 18px; background: var(--accent); border-radius: 50%; border: 3px solid var(--white); box-shadow: var(--shadow); }
|
||
.timeline-date { font-size: 13px; color: var(--gray-500); margin-bottom: 4px; }
|
||
.timeline-content { font-weight: 600; }
|
||
|
||
/* Badge */
|
||
.badge { display: inline-block; padding: 2px 8px; border-radius: 12px; font-size: 11px; font-weight: 600; }
|
||
.badge-blue { background: #bee3f8; color: #2c5282; }
|
||
.badge-green { background: #c6f6d5; color: #22543d; }
|
||
.badge-red { background: #fed7d7; color: #742a2a; }
|
||
.badge-yellow { background: #fefcbf; color: #744210; }
|
||
|
||
/* Responsive */
|
||
@media (max-width: 1024px) {
|
||
.main { margin-left: 0; }
|
||
.sidebar { transform: translateX(-100%); }
|
||
.sidebar.open { transform: translateX(0); }
|
||
.menu-toggle { display: block; }
|
||
}
|
||
@media (max-width: 768px) {
|
||
.kpi-grid { grid-template-columns: 1fr; }
|
||
.form-row { grid-template-columns: 1fr; }
|
||
.header { padding: 12px 16px; }
|
||
.content { padding: 16px; }
|
||
}
|
||
</style>
|
||
</head>
|
||
<body>
|
||
|
||
<div class="app">
|
||
<!-- Sidebar -->
|
||
<aside class="sidebar" id="sidebar">
|
||
<div class="sidebar-header">
|
||
<div class="sidebar-title">💧 Коммунальный бюджет</div>
|
||
</div>
|
||
<nav class="sidebar-nav">
|
||
<div class="nav-item active" data-page="dashboard">
|
||
<span class="nav-icon">📊</span>
|
||
<span>Главная</span>
|
||
</div>
|
||
<div class="nav-item" data-page="regulations">
|
||
<span class="nav-icon">📋</span>
|
||
<span>Регламент</span>
|
||
</div>
|
||
<div class="nav-item" data-page="budget">
|
||
<span class="nav-icon">💰</span>
|
||
<span>Бюджет</span>
|
||
</div>
|
||
<div class="nav-item" data-page="tariffs">
|
||
<span class="nav-icon">🏷️</span>
|
||
<span>Тарифы</span>
|
||
</div>
|
||
<div class="nav-item" data-page="consumption">
|
||
<span class="nav-icon">📈</span>
|
||
<span>Потребление</span>
|
||
</div>
|
||
<div class="nav-item" data-page="planfact">
|
||
<span class="nav-icon">⚖️</span>
|
||
<span>План-Факт</span>
|
||
</div>
|
||
<div class="nav-item" data-page="reports">
|
||
<span class="nav-icon">📑</span>
|
||
<span>Отчеты</span>
|
||
</div>
|
||
<div class="nav-item" data-page="settings">
|
||
<span class="nav-icon">⚙️</span>
|
||
<span>Настройки</span>
|
||
</div>
|
||
</nav>
|
||
</aside>
|
||
|
||
<!-- Main -->
|
||
<main class="main">
|
||
<!-- Header -->
|
||
<header class="header">
|
||
<div class="header-left">
|
||
<button class="menu-toggle" id="menuToggle">☰</button>
|
||
<select class="year-select" id="yearSelect" onchange="app.changeYear()">
|
||
<option value="2025">2025</option>
|
||
<option value="2026" selected>2026</option>
|
||
<option value="2027">2027</option>
|
||
</select>
|
||
</div>
|
||
<div class="header-right">
|
||
<button class="notification-btn">
|
||
🔔
|
||
<span class="notification-badge"></span>
|
||
</button>
|
||
<div class="user-menu">
|
||
<div class="user-avatar">Ж</div>
|
||
<span>Жанна</span>
|
||
</div>
|
||
</div>
|
||
</header>
|
||
|
||
<!-- Content -->
|
||
<div class="content">
|
||
|
||
<!-- Dashboard -->
|
||
<div class="page active" id="page-dashboard">
|
||
<div class="page-header">
|
||
<h1 class="page-title">Dashboard</h1>
|
||
<p class="page-subtitle">Обзор коммунального бюджета на 2026 год</p>
|
||
</div>
|
||
|
||
<!-- KPI Cards -->
|
||
<div class="kpi-grid" id="kpiGrid"></div>
|
||
|
||
<!-- Alerts -->
|
||
<div id="alertsContainer"></div>
|
||
|
||
<!-- Charts Row -->
|
||
<div style="display: grid; grid-template-columns: 2fr 1fr; gap: 24px; margin-bottom: 24px;">
|
||
<!-- Plan/Fact Chart -->
|
||
<div class="card">
|
||
<div class="card-header">
|
||
<h3 class="card-title">План / Факт / Прогноз</h3>
|
||
</div>
|
||
<div class="card-body">
|
||
<div class="chart-container" id="planFactChart"></div>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- Structure Chart -->
|
||
<div class="card">
|
||
<div class="card-header">
|
||
<h3 class="card-title">Структура расходов</h3>
|
||
</div>
|
||
<div class="card-body">
|
||
<div class="chart-container" id="structureChart"></div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- Recent Activity -->
|
||
<div class="card">
|
||
<div class="card-header">
|
||
<h3 class="card-title">Журнал изменений</h3>
|
||
</div>
|
||
<div class="card-body">
|
||
<div class="table-container">
|
||
<table id="auditTable"></table>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- Regulations -->
|
||
<div class="page" id="page-regulations">
|
||
<div class="page-header">
|
||
<h1 class="page-title">Регламент</h1>
|
||
<p class="page-subtitle">Электронная версия утвержденного регламента</p>
|
||
</div>
|
||
<div class="card">
|
||
<div class="card-body" id="regulationsContent"></div>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- Budget -->
|
||
<div class="page" id="page-budget">
|
||
<div class="page-header">
|
||
<h1 class="page-title">Бюджет</h1>
|
||
<p class="page-subtitle">Годовой и помесячный бюджет коммунальных расходов</p>
|
||
</div>
|
||
<div class="card">
|
||
<div class="card-header">
|
||
<h3 class="card-title">Годовой бюджет 2026</h3>
|
||
<button class="btn btn-primary btn-sm" onclick="app.openBudgetModal()">+ Добавить статью</button>
|
||
</div>
|
||
<div class="card-body">
|
||
<div class="table-container">
|
||
<table id="budgetTable"></table>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- Tariffs -->
|
||
<div class="page" id="page-tariffs">
|
||
<div class="page-header">
|
||
<h1 class="page-title">Тарифы</h1>
|
||
<p class="page-subtitle">Управление тарифами и история изменений</p>
|
||
</div>
|
||
<div class="card">
|
||
<div class="card-header">
|
||
<h3 class="card-title">Текущие тарифы</h3>
|
||
<button class="btn btn-primary btn-sm" onclick="app.openTariffModal()">+ Добавить тариф</button>
|
||
</div>
|
||
<div class="card-body">
|
||
<div class="table-container">
|
||
<table id="tariffTable"></table>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- Consumption -->
|
||
<div class="page" id="page-consumption">
|
||
<div class="page-header">
|
||
<h1 class="page-title">Потребление</h1>
|
||
<p class="page-subtitle">Ввод показаний и история потребления</p>
|
||
</div>
|
||
<div class="card">
|
||
<div class="card-header">
|
||
<h3 class="card-title">Внести показания</h3>
|
||
<button class="btn btn-primary btn-sm" onclick="app.openReadingModal()">+ Внести показания</button>
|
||
</div>
|
||
<div class="card-body">
|
||
<div class="table-container">
|
||
<table id="consumptionTable"></table>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- Plan-Fact -->
|
||
<div class="page" id="page-planfact">
|
||
<div class="page-header">
|
||
<h1 class="page-title">План-Факт анализ</h1>
|
||
<p class="page-subtitle">Сравнение плановых и фактических показателей</p>
|
||
</div>
|
||
<div class="card">
|
||
<div class="card-header">
|
||
<h3 class="card-title">Анализ отклонений</h3>
|
||
</div>
|
||
<div class="card-body">
|
||
<div class="table-container">
|
||
<table id="planFactTable"></table>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- Reports -->
|
||
<div class="page" id="page-reports">
|
||
<div class="page-header">
|
||
<h1 class="page-title">Отчеты</h1>
|
||
<p class="page-subtitle">Формирование и экспорт отчетов</p>
|
||
</div>
|
||
<div class="card">
|
||
<div class="card-body">
|
||
<div style="display: grid; grid-template-columns: repeat(auto-fit, minmax(250px, 1fr)); gap: 16px;">
|
||
<div class="card" style="margin:0; cursor: pointer;" onclick="app.exportReport('budget')">
|
||
<div class="card-body">
|
||
<h4 style="margin-bottom: 8px;">📊 Исполнение бюджета</h4>
|
||
<p style="color: var(--gray-500); font-size: 13px;">Отчет по исполнению коммунального бюджета</p>
|
||
</div>
|
||
</div>
|
||
<div class="card" style="margin:0; cursor: pointer;" onclick="app.exportReport('planfact')">
|
||
<div class="card-body">
|
||
<h4 style="margin-bottom: 8px;">⚖️ План-Факт</h4>
|
||
<p style="color: var(--gray-500); font-size: 13px;">Сравнение плановых и фактических показателей</p>
|
||
</div>
|
||
</div>
|
||
<div class="card" style="margin:0; cursor: pointer;" onclick="app.exportReport('tariffs')">
|
||
<div class="card-body">
|
||
<h4 style="margin-bottom: 8px;">🏷️ Динамика тарифов</h4>
|
||
<p style="color: var(--gray-500); font-size: 13px;">История изменения тарифов</p>
|
||
</div>
|
||
</div>
|
||
<div class="card" style="margin:0; cursor: pointer;" onclick="app.exportReport('consumption')">
|
||
<div class="card-body">
|
||
<h4 style="margin-bottom: 8px;">📈 Динамика потребления</h4>
|
||
<p style="color: var(--gray-500); font-size: 13px;">Анализ потребления по месяцам</p>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- Settings -->
|
||
<div class="page" id="page-settings">
|
||
<div class="page-header">
|
||
<h1 class="page-title">Настройки</h1>
|
||
<p class="page-subtitle">Справочники и параметры системы</p>
|
||
</div>
|
||
<div class="card">
|
||
<div class="card-header">
|
||
<h3 class="card-title">Справочник коммунальных услуг</h3>
|
||
</div>
|
||
<div class="card-body">
|
||
<div id="servicesList"></div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
</div>
|
||
</main>
|
||
</div>
|
||
|
||
<!-- Modal Tariff -->
|
||
<div class="modal" id="tariffModal">
|
||
<div class="modal-content">
|
||
<div class="modal-header">
|
||
<h3 class="modal-title">Добавить тариф</h3>
|
||
<button class="modal-close" onclick="app.closeModal('tariffModal')">×</button>
|
||
</div>
|
||
<div class="modal-body">
|
||
<form id="tariffForm">
|
||
<div class="form-group">
|
||
<label class="form-label">Коммунальная услуга *</label>
|
||
<select class="form-select" id="tariffService" required></select>
|
||
</div>
|
||
<div class="form-group">
|
||
<label class="form-label">Поставщик *</label>
|
||
<input type="text" class="form-input" id="tariffSupplier" required>
|
||
</div>
|
||
<div class="form-row">
|
||
<div class="form-group">
|
||
<label class="form-label">Единица измерения *</label>
|
||
<select class="form-select" id="tariffUnit" required>
|
||
<option value="кВт·ч">кВт·ч</option>
|
||
<option value="Гкал">Гкал</option>
|
||
<option value="м³">м³</option>
|
||
<option value="тонна">тонна</option>
|
||
</select>
|
||
</div>
|
||
<div class="form-group">
|
||
<label class="form-label">Тариф *</label>
|
||
<input type="number" step="0.01" class="form-input" id="tariffRate" required>
|
||
</div>
|
||
</div>
|
||
<div class="form-row">
|
||
<div class="form-group">
|
||
<label class="form-label">НДС %</label>
|
||
<input type="number" step="0.1" class="form-input" id="tariffVat" value="12">
|
||
</div>
|
||
<div class="form-group">
|
||
<label class="form-label">Дата начала *</label>
|
||
<input type="date" class="form-input" id="tariffDateFrom" required>
|
||
</div>
|
||
</div>
|
||
<div class="form-group">
|
||
<label class="form-label">Дата окончания</label>
|
||
<input type="date" class="form-input" id="tariffDateTo">
|
||
</div>
|
||
<div class="form-group">
|
||
<label class="form-label">Основание изменения</label>
|
||
<input type="text" class="form-input" id="tariffBasis">
|
||
</div>
|
||
<div class="form-group">
|
||
<label class="form-label">Комментарий</label>
|
||
<textarea class="form-input" id="tariffComment" rows="3"></textarea>
|
||
</div>
|
||
</form>
|
||
</div>
|
||
<div class="modal-footer">
|
||
<button class="btn btn-secondary" onclick="app.closeModal('tariffModal')">Отмена</button>
|
||
<button class="btn btn-primary" onclick="app.saveTariff()">Сохранить</button>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<script>
|
||
// Data Store
|
||
const store = {
|
||
year: 2026,
|
||
services: ['Электроэнергия', 'Теплоснабжение', 'Водоснабжение', 'Водоотведение', 'Газ', 'ТБО', 'Прочее'],
|
||
tariffs: [
|
||
{ id: 1, service: 'Электроэнергия', supplier: 'АО "ЭК Астана"', unit: 'кВт·ч', rate: 28.10, vat: 12, dateFrom: '2026-07-01', dateTo: null, status: 'active' },
|
||
{ id: 2, service: 'Электроэнергия', supplier: 'АО "ЭК Астана"', unit: 'кВт·ч', rate: 25.40, vat: 12, dateFrom: '2026-01-01', dateTo: '2026-06-30', status: 'archived' },
|
||
{ id: 3, service: 'Теплоснабжение', supplier: 'ТОО "ТеплоКом"', unit: 'Гкал', rate: 4200, vat: 12, dateFrom: '2026-01-01', dateTo: null, status: 'active' },
|
||
{ id: 4, service: 'Водоснабжение', supplier: 'ТОО "ВодаСервис"', unit: 'м³', rate: 520, vat: 12, dateFrom: '2026-04-01', dateTo: null, status: 'active' },
|
||
{ id: 5, service: 'Водоснабжение', supplier: 'ТОО "ВодаСервис"', unit: 'м³', rate: 480, vat: 12, dateFrom: '2026-01-01', dateTo: '2026-03-31', status: 'archived' },
|
||
{ id: 6, service: 'Водоотведение', supplier: 'ТОО "ВодаСервис"', unit: 'м³', rate: 380, vat: 12, dateFrom: '2026-01-01', dateTo: null, status: 'active' },
|
||
{ id: 7, service: 'Газ', supplier: 'ТОО "ГазКаз"', unit: 'м³', rate: 18.50, vat: 12, dateFrom: '2026-01-01', dateTo: null, status: 'active' },
|
||
],
|
||
consumption: [
|
||
{ month: 'Янв', electricity: 10200, heat: 180, water: 520, sewerage: 520 },
|
||
{ month: 'Фев', electricity: 10400, heat: 175, water: 530, sewerage: 530 },
|
||
{ month: 'Мар', electricity: 9800, heat: 160, water: 510, sewerage: 510 },
|
||
{ month: 'Апр', electricity: 9200, heat: 140, water: 490, sewerage: 490 },
|
||
{ month: 'Май', electricity: 8800, heat: 100, water: 480, sewerage: 480 },
|
||
{ month: 'Июн', electricity: 8600, heat: 60, water: 500, sewerage: 500 },
|
||
{ month: 'Июл', electricity: 9000, heat: 40, water: 550, sewerage: 550 },
|
||
{ month: 'Авг', electricity: 9400, heat: 45, water: 560, sewerage: 560 },
|
||
{ month: 'Сен', electricity: 9600, heat: 80, water: 520, sewerage: 520 },
|
||
{ month: 'Окт', electricity: 10000, heat: 130, water: 510, sewerage: 510 },
|
||
{ month: 'Ноя', electricity: 10600, heat: 170, water: 530, sewerage: 530 },
|
||
{ month: 'Дек', electricity: 11000, heat: 185, water: 540, sewerage: 540 },
|
||
],
|
||
budget: {
|
||
plan: [1520, 1540, 1480, 1380, 1280, 1180, 1220, 1260, 1320, 1420, 1520, 1580],
|
||
fact: [1480, 1560, 1450, 1360, 1250, 1200, 1380, 1420, 1300, 1400, 1500, null],
|
||
},
|
||
auditLog: [
|
||
{ date: '21.08.26', user: 'Жанна', object: 'Тариф', change: 'Электроэнергия: 25.40 → 28.10 ₸' },
|
||
{ date: '20.08.26', user: 'Жанна', object: 'Бюджет', change: 'Перерасчет бюджета на II полугодие' },
|
||
{ date: '19.08.26', user: 'Бухгалтер', object: 'Факт', change: 'Внесены фактические расходы за Июль' },
|
||
]
|
||
};
|
||
|
||
// Application
|
||
const app = {
|
||
init() {
|
||
this.renderNavigation();
|
||
this.renderDashboard();
|
||
this.renderTariffs();
|
||
this.renderBudget();
|
||
this.renderConsumption();
|
||
this.renderPlanFact();
|
||
this.renderRegulations();
|
||
this.renderSettings();
|
||
this.setupEventListeners();
|
||
},
|
||
|
||
changeYear() {
|
||
store.year = parseInt(document.getElementById('yearSelect').value);
|
||
this.renderDashboard();
|
||
this.renderBudget();
|
||
this.renderPlanFact();
|
||
},
|
||
|
||
renderNavigation() {
|
||
document.querySelectorAll('.nav-item').forEach(item => {
|
||
item.addEventListener('click', () => {
|
||
document.querySelectorAll('.nav-item').forEach(i => i.classList.remove('active'));
|
||
item.classList.add('active');
|
||
const page = item.dataset.page;
|
||
document.querySelectorAll('.page').forEach(p => p.classList.remove('active'));
|
||
document.getElementById(`page-${page}`).classList.add('active');
|
||
document.getElementById('sidebar').classList.remove('open');
|
||
});
|
||
});
|
||
},
|
||
|
||
renderDashboard() {
|
||
const totalPlan = store.budget.plan.reduce((a, b) => a + b, 0) * 1000;
|
||
const totalFact = store.budget.fact.filter(x => x).reduce((a, b) => a + b, 0) * 1000;
|
||
const forecast = totalPlan * 0.98;
|
||
const deviation = totalFact - totalPlan;
|
||
const deviationPercent = ((deviation / totalPlan) * 100).toFixed(2);
|
||
const accuracy = (100 - Math.abs(deviationPercent)).toFixed(2);
|
||
|
||
document.getElementById('kpiGrid').innerHTML = `
|
||
<div class="kpi-card">
|
||
<div class="kpi-label">Годовой бюджет</div>
|
||
<div class="kpi-value">${(totalPlan / 1000000).toFixed(1)} млн ₸</div>
|
||
</div>
|
||
<div class="kpi-card ${deviation < 0 ? 'success' : 'danger'}">
|
||
<div class="kpi-label">Фактические расходы</div>
|
||
<div class="kpi-value">${(totalFact / 1000000).toFixed(1)} млн ₸</div>
|
||
<div class="kpi-delta ${deviation < 0 ? 'positive' : 'negative'}">
|
||
${deviation < 0 ? '↓' : '↑'} ${Math.abs(deviation / 1000).toFixed(0)} тыс. ₸
|
||
</div>
|
||
</div>
|
||
<div class="kpi-card">
|
||
<div class="kpi-label">Прогноз до конца года</div>
|
||
<div class="kpi-value">${(forecast / 1000000).toFixed(1)} млн ₸</div>
|
||
</div>
|
||
<div class="kpi-card ${deviation < 0 ? 'success' : 'danger'}">
|
||
<div class="kpi-label">Отклонение</div>
|
||
<div class="kpi-value">${(deviation / 1000).toFixed(0)} тыс. ₸</div>
|
||
<div class="kpi-delta ${deviation < 0 ? 'positive' : 'negative'}">${deviationPercent}%</div>
|
||
</div>
|
||
<div class="kpi-card success">
|
||
<div class="kpi-label">Точность бюджета</div>
|
||
<div class="kpi-value">${accuracy}%</div>
|
||
<div class="kpi-delta positive">Цель: ≥97%</div>
|
||
</div>
|
||
<div class="kpi-card">
|
||
<div class="kpi-label">Исполнение бюджета</div>
|
||
<div class="kpi-value">${((totalFact / totalPlan) * 100).toFixed(1)}%</div>
|
||
</div>
|
||
`;
|
||
|
||
// Alerts
|
||
const alerts = `
|
||
<div class="alert alert-warning">
|
||
<span class="alert-icon">⚠️</span>
|
||
<div class="alert-content">
|
||
<div class="alert-title">Водоснабжение</div>
|
||
<div class="alert-text">Фактическое потребление превышает план на 8,4%. План: 6 500 м³, Факт: 7 046 м³</div>
|
||
</div>
|
||
<button class="btn btn-sm btn-secondary">Подробнее</button>
|
||
</div>
|
||
<div class="alert alert-danger">
|
||
<span class="alert-icon">🔴</span>
|
||
<div class="alert-content">
|
||
<div class="alert-title">Электроэнергия</div>
|
||
<div class="alert-text">Обнаружено изменение тарифа. Старый: 25,40 ₸, Новый: 28,10 ₸ с 01.07.2026</div>
|
||
</div>
|
||
<button class="btn btn-sm btn-primary" onclick="alert('Бюджет пересчитан')">Пересчитать</button>
|
||
</div>
|
||
`;
|
||
document.getElementById('alertsContainer').innerHTML = alerts;
|
||
|
||
// Audit Log
|
||
const auditRows = store.auditLog.map(log => `
|
||
<tr>
|
||
<td>${log.date}</td>
|
||
<td>${log.user}</td>
|
||
<td>${log.object}</td>
|
||
<td>${log.change}</td>
|
||
</tr>
|
||
`).join('');
|
||
document.getElementById('auditTable').innerHTML = `
|
||
<thead><tr><th>Дата</th><th>Пользователь</th><th>Объект</th><th>Изменение</th></tr></thead>
|
||
<tbody>${auditRows}</tbody>
|
||
`;
|
||
|
||
// Charts (simple CSS-based)
|
||
this.renderCharts();
|
||
},
|
||
|
||
renderCharts() {
|
||
const months = ['Янв', 'Фев', 'Мар', 'Апр', 'Май', 'Июн', 'Июл', 'Авг', 'Сен', 'Окт', 'Ноя', 'Дек'];
|
||
|
||
// Plan/Fact Chart
|
||
const maxVal = Math.max(...store.budget.plan, ...store.budget.fact.filter(x => x)) * 1.1;
|
||
let planFactHtml = '<div style="display: flex; align-items: flex-end; height: 250px; gap: 8px; padding: 20px 0;">';
|
||
for (let i = 0; i < 12; i++) {
|
||
const planH = (store.budget.plan[i] / maxVal) * 100;
|
||
const factH = store.budget.fact[i] ? (store.budget.fact[i] / maxVal) * 100 : 0;
|
||
planFactHtml += `
|
||
<div style="flex: 1; display: flex; flex-direction: column; align-items: center; gap: 4px;">
|
||
<div style="display: flex; gap: 2px; align-items: flex-end; height: 200px;">
|
||
<div style="width: 12px; height: ${planH}%; background: #3182ce; border-radius: 4px 4px 0 0;" title="План: ${store.budget.plan[i]}"></div>
|
||
<div style="width: 12px; height: ${factH}%; background: ${store.budget.fact[i] && store.budget.fact[i] > store.budget.plan[i] ? '#e53e3e' : '#38a169'}; border-radius: 4px 4px 0 0;" title="Факт: ${store.budget.fact[i] || 'н/д'}"></div>
|
||
</div>
|
||
<span style="font-size: 11px; color: var(--gray-500);">${months[i]}</span>
|
||
</div>
|
||
`;
|
||
}
|
||
planFactHtml += '</div>';
|
||
document.getElementById('planFactChart').innerHTML = planFactHtml;
|
||
|
||
// Structure Chart (simple pie-like with bars)
|
||
const structure = [
|
||
{ name: 'Электроэнергия', percent: 32, color: '#3182ce' },
|
||
{ name: 'Теплоснабжение', percent: 38, color: '#e53e3e' },
|
||
{ name: 'Водоснабжение', percent: 15, color: '#38a169' },
|
||
{ name: 'Водоотведение', percent: 8, color: '#d69e2e' },
|
||
{ name: 'Газ', percent: 5, color: '#805ad5' },
|
||
{ name: 'Прочее', percent: 2, color: '#718096' },
|
||
];
|
||
let structureHtml = '<div style="padding: 20px;">';
|
||
structure.forEach(item => {
|
||
structureHtml += `
|
||
<div style="display: flex; align-items: center; gap: 12px; margin-bottom: 12px;">
|
||
<div style="width: 12px; height: 12px; background: ${item.color}; border-radius: 3px;"></div>
|
||
<span style="flex: 1; font-size: 13px;">${item.name}</span>
|
||
<span style="font-weight: 600;">${item.percent}%</span>
|
||
</div>
|
||
<div style="height: 8px; background: var(--gray-200); border-radius: 4px; margin-bottom: 16px; overflow: hidden;">
|
||
<div style="width: ${item.percent}%; height: 100%; background: ${item.color};"></div>
|
||
</div>
|
||
`;
|
||
});
|
||
structureHtml += '</div>';
|
||
document.getElementById('structureChart').innerHTML = structureHtml;
|
||
},
|
||
|
||
renderTariffs() {
|
||
const rows = store.tariffs.map(t => `
|
||
<tr>
|
||
<td>${t.service}</td>
|
||
<td>${t.supplier}</td>
|
||
<td>${t.unit}</td>
|
||
<td>${t.rate.toFixed(2)} ₸</td>
|
||
<td>${t.dateFrom.slice(5)}</td>
|
||
<td>${t.dateTo ? t.dateTo.slice(5) : '—'}</td>
|
||
<td><span class="status ${t.status}">${t.status === 'active' ? 'Активен' : 'Архив'}</span></td>
|
||
<td>
|
||
<button class="btn-icon" onclick="alert('Редактирование')">✏️</button>
|
||
<button class="btn-icon" onclick="this.closest('tr').remove()">🗑️</button>
|
||
</td>
|
||
</tr>
|
||
`).join('');
|
||
document.getElementById('tariffTable').innerHTML = `
|
||
<thead><tr><th>Услуга</th><th>Поставщик</th><th>Ед.</th><th>Тариф</th><th>Начало</th><th>Окончание</th><th>Статус</th><th>Действия</th></tr></thead>
|
||
<tbody>${rows}</tbody>
|
||
`;
|
||
|
||
// Service select in modal
|
||
const options = store.services.map(s => `<option value="${s}">${s}</option>`).join('');
|
||
document.getElementById('tariffService').innerHTML = options;
|
||
},
|
||
|
||
renderBudget() {
|
||
const months = ['Янв', 'Фев', 'Мар', 'Апр', 'Май', 'Июн', 'Июл', 'Авг', 'Сен', 'Окт', 'Ноя', 'Дек'];
|
||
const services = ['Электроэнергия', 'Теплоснабжение', 'Водоснабжение', 'Водоотведение', 'Газ', 'ИТОГО'];
|
||
|
||
let html = '<thead><tr><th>Услуга</th>';
|
||
months.forEach(m => html += `<th>${m}</th>`);
|
||
html += '<th>Год</th></tr></thead><tbody>';
|
||
|
||
services.forEach(service => {
|
||
html += `<tr><td><strong>${service}</strong></td>`;
|
||
let yearTotal = 0;
|
||
for (let i = 0; i < 12; i++) {
|
||
const val = Math.round(Math.random() * 200 + 50);
|
||
yearTotal += val;
|
||
html += `<td>${val}</td>`;
|
||
}
|
||
html += `<td><strong>${yearTotal}</strong></td></tr>`;
|
||
});
|
||
html += '</tbody>';
|
||
document.getElementById('budgetTable').innerHTML = html;
|
||
},
|
||
|
||
renderConsumption() {
|
||
const rows = store.consumption.map((c, i) => `
|
||
<tr>
|
||
<td>${c.month}</td>
|
||
<td>${c.electricity}</td>
|
||
<td>${c.heat}</td>
|
||
<td>${c.water}</td>
|
||
<td>${c.sewerage}</td>
|
||
</tr>
|
||
`).join('');
|
||
document.getElementById('consumptionTable').innerHTML = `
|
||
<thead><tr><th>Месяц</th><th>Электроэнергия (кВт·ч)</th><th>Тепло (Гкал)</th><th>Вода (м³)</th><th>Водоотведение (м³)</th></tr></thead>
|
||
<tbody>${rows}</tbody>
|
||
`;
|
||
},
|
||
|
||
renderPlanFact() {
|
||
const months = ['Янв', 'Фев', 'Мар', 'Апр', 'Май', 'Июн', 'Июл', 'Авг'];
|
||
const rows = months.map((m, i) => {
|
||
const plan = store.budget.plan[i];
|
||
const fact = store.budget.fact[i] || 0;
|
||
const dev = fact - plan;
|
||
const devPercent = ((dev / plan) * 100).toFixed(1);
|
||
return `
|
||
<tr>
|
||
<td>${m}</td>
|
||
<td>${plan.toLocaleString()} ₸</td>
|
||
<td>${fact.toLocaleString()} ₸</td>
|
||
<td style="color: ${dev > 0 ? 'var(--danger)' : 'var(--success)'}">${dev > 0 ? '+' : ''}${dev.toLocaleString()} ₸</td>
|
||
<td style="color: ${dev > 0 ? 'var(--danger)' : 'var(--success)'}">${dev > 0 ? '+' : ''}${devPercent}%</td>
|
||
<td>${dev > 0 ? 'Рост тарифа' : 'Экономия'}</td>
|
||
</tr>
|
||
`;
|
||
}).join('');
|
||
document.getElementById('planFactTable').innerHTML = `
|
||
<thead><tr><th>Месяц</th><th>План</th><th>Факт</th><th>Отклонение ₸</th><th>Отклонение %</th><th>Причина</th></tr></thead>
|
||
<tbody>${rows}</tbody>
|
||
`;
|
||
},
|
||
|
||
renderRegulations() {
|
||
const chapters = [
|
||
'1. Общие положения',
|
||
'2. Ответственные подразделения',
|
||
'3. Исходные данные',
|
||
'4. Прогнозирование потребления',
|
||
'5. Учет изменения тарифов',
|
||
'6. Формирование бюджета',
|
||
'7. Контроль исполнения',
|
||
'8. Анализ отклонений',
|
||
'9. Корректировка бюджета',
|
||
'10. Отчетность',
|
||
'11. Контрольные показатели',
|
||
'12. Хранение документов'
|
||
];
|
||
document.getElementById('regulationsContent').innerHTML = `
|
||
<div style="display: grid; grid-template-columns: repeat(auto-fit, minmax(300px, 1fr)); gap: 16px;">
|
||
${chapters.map(c => `
|
||
<div style="padding: 16px; background: var(--gray-50); border-radius: var(--radius); cursor: pointer;" onclick="alert('Открытие раздела: ${c}')">
|
||
<strong>${c}</strong>
|
||
<p style="color: var(--gray-500); font-size: 13px; margin-top: 8px;">Нажмите для просмотра содержания раздела</p>
|
||
</div>
|
||
`).join('')}
|
||
</div>
|
||
`;
|
||
},
|
||
|
||
renderSettings() {
|
||
document.getElementById('servicesList').innerHTML = `
|
||
<div style="display: grid; grid-template-columns: repeat(auto-fit, minmax(200px, 1fr)); gap: 12px;">
|
||
${store.services.map(s => `
|
||
<div style="padding: 12px; background: var(--gray-50); border-radius: var(--radius); display: flex; justify-content: space-between; align-items: center;">
|
||
<span>${s}</span>
|
||
<button class="btn-icon" onclick="alert('Редактирование')">✏️</button>
|
||
</div>
|
||
`).join('')}
|
||
</div>
|
||
<button class="btn btn-primary btn-sm" style="margin-top: 16px;" onclick="alert('Добавление услуги')">+ Добавить услугу</button>
|
||
`;
|
||
},
|
||
|
||
openTariffModal() {
|
||
document.getElementById('tariffModal').classList.add('open');
|
||
},
|
||
|
||
openBudgetModal() {
|
||
alert('Функция добавления статьи бюджета');
|
||
},
|
||
|
||
openReadingModal() {
|
||
alert('Функция внесения показаний');
|
||
},
|
||
|
||
closeModal(id) {
|
||
document.getElementById(id).classList.remove('open');
|
||
},
|
||
|
||
saveTariff() {
|
||
const service = document.getElementById('tariffService').value;
|
||
const supplier = document.getElementById('tariffSupplier').value;
|
||
const rate = parseFloat(document.getElementById('tariffRate').value);
|
||
const dateFrom = document.getElementById('tariffDateFrom').value;
|
||
|
||
if (!service || !supplier || !rate || !dateFrom) {
|
||
alert('Заполните обязательные поля');
|
||
return;
|
||
}
|
||
|
||
store.tariffs.unshift({
|
||
id: Date.now(),
|
||
service,
|
||
supplier,
|
||
unit: document.getElementById('tariffUnit').value,
|
||
rate,
|
||
vat: parseFloat(document.getElementById('tariffVat').value) || 0,
|
||
dateFrom,
|
||
dateTo: document.getElementById('tariffDateTo').value || null,
|
||
status: 'active'
|
||
});
|
||
|
||
this.renderTariffs();
|
||
this.closeModal('tariffModal');
|
||
document.getElementById('tariffForm').reset();
|
||
|
||
// Add to audit log
|
||
store.auditLog.unshift({
|
||
date: new Date().toLocaleDateString('ru-RU', {day: '2-digit', month: '2-digit', year: '2-digit'}),
|
||
user: 'Жанна',
|
||
object: 'Тариф',
|
||
change: `${service}: новый тариф ${rate} ₸ от ${dateFrom.slice(5)}`
|
||
});
|
||
this.renderDashboard();
|
||
},
|
||
|
||
exportReport(type) {
|
||
alert(`Экспорт отчета "${type}" в Excel...`);
|
||
// В реальной реализации здесь будет генерация XLSX
|
||
},
|
||
|
||
setupEventListeners() {
|
||
document.getElementById('menuToggle').addEventListener('click', () => {
|
||
document.getElementById('sidebar').classList.toggle('open');
|
||
});
|
||
|
||
// Close modal on outside click
|
||
document.querySelectorAll('.modal').forEach(modal => {
|
||
modal.addEventListener('click', (e) => {
|
||
if (e.target === modal) modal.classList.remove('open');
|
||
});
|
||
});
|
||
}
|
||
};
|
||
|
||
// Initialize app
|
||
app.init();
|
||
</script>
|
||
</body>
|
||
</html>
|