tehnicheskoe-zadanie-sayt-eu/index.html

905 lines
40 KiB
HTML
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

<!DOCTYPE html>
<html lang="ru">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width,initial-scale=1">
<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')">&times;</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>