490 lines
20 KiB
HTML
490 lines
20 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;600;700;800&display=swap" rel="stylesheet">
|
||
<style>
|
||
:root{--ink:#0F1218;--cyan:#00E5FF;--cyan-50:#E8FCFF;--white:#fff;--gray-500:#5B6573;--gray-100:#F2F4F7;--green:#10B981;--red:#EF4444;--yellow:#F59E0B}
|
||
*{box-sizing:border-box;margin:0;padding:0}
|
||
body{font-family:'Inter',sans-serif;font-size:15px;line-height:1.5;color:var(--ink);background:var(--gray-100)}
|
||
.header{background:var(--ink);color:var(--white);padding:24px 32px}
|
||
.header h1{font-size:24px;font-weight:800}
|
||
.header p{color:#9aa3b2;font-size:14px}
|
||
.container{max-width:1400px;margin:0 auto;padding:32px 24px}
|
||
.stats{display:grid;grid-template-columns:repeat(5,1fr);gap:16px;margin-bottom:32px}
|
||
.stat-card{background:var(--white);border-radius:12px;padding:20px}
|
||
.stat-card .label{font-size:13px;color:var(--gray-500);margin-bottom:4px}
|
||
.stat-card .value{font-size:28px;font-weight:800}
|
||
.stat-card .value.green{color:var(--green)}
|
||
.stat-card .value.yellow{color:var(--yellow)}
|
||
.stat-card .value.red{color:var(--red)}
|
||
.card{background:var(--white);border-radius:12px;padding:24px;margin-bottom:16px}
|
||
.card h2{font-size:18px;font-weight:700;margin-bottom:16px}
|
||
table{width:100%;border-collapse:collapse}
|
||
th,td{text-align:left;padding:12px 16px;border-bottom:1px solid var(--gray-100);font-size:14px}
|
||
th{font-weight:600;color:var(--gray-500);font-size:12px;text-transform:uppercase}
|
||
tr:hover{background:var(--cyan-50)}
|
||
.btn{display:inline-block;padding:8px 16px;border-radius:6px;font-weight:600;font-size:13px;text-decoration:none;cursor:pointer;border:none}
|
||
.btn-primary{background:var(--cyan);color:var(--ink)}
|
||
.btn-primary:hover{background:#1be5ff}
|
||
.btn-success{background:var(--green);color:var(--white)}
|
||
.btn-download{background:var(--gray-100);color:var(--ink)}
|
||
.filter-bar{display:flex;gap:12px;margin-bottom:16px;flex-wrap:wrap;align-items:center}
|
||
.filter-bar input,.filter-bar select{padding:8px 12px;border:2px solid var(--gray-100);border-radius:6px;font-size:14px}
|
||
.tabs{display:flex;gap:8px;margin-bottom:24px}
|
||
.tab{padding:10px 20px;border-radius:8px;font-weight:600;cursor:pointer;border:none;background:var(--white);color:var(--ink)}
|
||
.tab.active{background:var(--cyan);color:var(--ink)}
|
||
.progress-bar{height:8px;background:var(--gray-100);border-radius:4px;overflow:hidden;margin-top:8px}
|
||
.progress-fill{height:100%;border-radius:4px;transition:width 0.3s}
|
||
.progress-fill.good{background:var(--green)}
|
||
.progress-fill.warning{background:var(--yellow)}
|
||
.progress-fill.danger{background:var(--red)}
|
||
.text-right{text-align:right}
|
||
.text-center{text-align:center}
|
||
.total-row{background:var(--cyan-50);font-weight:700}
|
||
.hidden{display:none}
|
||
@media(max-width:1024px){
|
||
.stats{grid-template-columns:repeat(2,1fr)}
|
||
}
|
||
@media(max-width:640px){
|
||
.stats{grid-template-columns:1fr}
|
||
table{font-size:12px}
|
||
th,td{padding:8px}
|
||
}
|
||
</style>
|
||
</head>
|
||
<body>
|
||
|
||
<div class="header">
|
||
<h1>Дашборд исполнения бюджета</h1>
|
||
<p>Мониторинг бюджета по статьям — филиалы Казахтелеком</p>
|
||
</div>
|
||
|
||
<div class="container">
|
||
<div class="tabs">
|
||
<button class="tab active" onclick="showTab('summary')">Сводная таблица</button>
|
||
<button class="tab" onclick="showTab('by-branch')">По филиалам</button>
|
||
<button class="tab" onclick="showTab('by-article')">По статьям</button>
|
||
</div>
|
||
|
||
<!-- Сводная таблица -->
|
||
<div id="tab-summary">
|
||
<div class="stats">
|
||
<div class="stat-card">
|
||
<div class="label">Общий план</div>
|
||
<div class="value" id="total-plan">0</div>
|
||
</div>
|
||
<div class="stat-card">
|
||
<div class="label">Общий факт</div>
|
||
<div class="value" id="total-fact">0</div>
|
||
</div>
|
||
<div class="stat-card">
|
||
<div class="label">Выполнение</div>
|
||
<div class="value green" id="total-percent">0%</div>
|
||
</div>
|
||
<div class="stat-card">
|
||
<div class="label">Остаток</div>
|
||
<div class="value yellow" id="total-remain">0</div>
|
||
</div>
|
||
<div class="stat-card">
|
||
<div class="label">Филиалов сдано</div>
|
||
<div class="value green" id="submitted-branches">0</div>
|
||
</div>
|
||
</div>
|
||
|
||
<div class="card">
|
||
<div class="filter-bar">
|
||
<select id="month-filter" onchange="filterData()">
|
||
<option value="all">Все месяцы</option>
|
||
<option value="Январь">Январь</option>
|
||
<option value="Февраль">Февраль</option>
|
||
<option value="Март">Март</option>
|
||
<option value="Апрель">Апрель</option>
|
||
<option value="Май">Май</option>
|
||
<option value="Июнь">Июнь</option>
|
||
<option value="Июль">Июль</option>
|
||
<option value="Август">Август</option>
|
||
<option value="Сентябрь">Сентябрь</option>
|
||
<option value="Октябрь">Октябрь</option>
|
||
<option value="Ноябрь">Ноябрь</option>
|
||
<option value="Декабрь">Декабрь</option>
|
||
</select>
|
||
<select id="article-filter" onchange="filterData()">
|
||
<option value="all">Все статьи</option>
|
||
<option value="2.4.5">2.4.5 - Командировочные расходы</option>
|
||
<option value="2.4.6">2.4.6 - Подготовка кадров</option>
|
||
<option value="4.1.6">4.1.6 - Командировочные расходы</option>
|
||
<option value="4.1.7">4.1.7 - Подготовка кадров</option>
|
||
</select>
|
||
<button class="btn btn-download" onclick="exportToExcel()">Скачать Excel</button>
|
||
<button class="btn btn-primary" onclick="addTestData()">Добавить тестовые данные</button>
|
||
</div>
|
||
|
||
<table id="summary-table">
|
||
<thead>
|
||
<tr>
|
||
<th>Статья</th>
|
||
<th class="text-right">План (тенге)</th>
|
||
<th class="text-right">Факт (тенге)</th>
|
||
<th class="text-right">Нарастающий итог</th>
|
||
<th class="text-center">Выполнение</th>
|
||
<th>Прогресс</th>
|
||
</tr>
|
||
</thead>
|
||
<tbody id="summary-body"></tbody>
|
||
<tfoot>
|
||
<tr class="total-row">
|
||
<td><strong>ИТОГО</strong></td>
|
||
<td class="text-right" id="foot-plan">0</td>
|
||
<td class="text-right" id="foot-fact">0</td>
|
||
<td class="text-right" id="foot-cumulative">0</td>
|
||
<td class="text-center" id="foot-percent">0%</td>
|
||
<td></td>
|
||
</tr>
|
||
</tfoot>
|
||
</table>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- По филиалам -->
|
||
<div id="tab-by-branch" class="hidden">
|
||
<div class="card">
|
||
<div class="filter-bar">
|
||
<input type="text" id="search-branch" placeholder="Поиск филиала..." oninput="filterBranchTable()">
|
||
<button class="btn btn-download" onclick="exportBranchesToExcel()">Скачать Excel</button>
|
||
</div>
|
||
<table id="branch-table">
|
||
<thead>
|
||
<tr>
|
||
<th>Филиал</th>
|
||
<th class="text-right">План (тенге)</th>
|
||
<th class="text-right">Факт (тенге)</th>
|
||
<th class="text-center">Выполнение</th>
|
||
<th>Прогресс</th>
|
||
<th>Статус</th>
|
||
</tr>
|
||
</thead>
|
||
<tbody id="branch-body"></tbody>
|
||
</table>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- По статьям -->
|
||
<div id="tab-by-article" class="hidden">
|
||
<div class="card">
|
||
<div class="filter-bar">
|
||
<select id="article-detail-filter" onchange="filterArticleTable()">
|
||
<option value="all">Все статьи</option>
|
||
<option value="2.4.5">2.4.5 - Командировочные расходы</option>
|
||
<option value="2.4.6">2.4.6 - Подготовка кадров</option>
|
||
<option value="4.1.6">4.1.6 - Командировочные расходы</option>
|
||
<option value="4.1.7">4.1.7 - Подготовка кадров</option>
|
||
</select>
|
||
<button class="btn btn-download" onclick="exportArticlesToExcel()">Скачать Excel</button>
|
||
</div>
|
||
<table id="article-table">
|
||
<thead>
|
||
<tr>
|
||
<th>Статья</th>
|
||
<th>Филиал</th>
|
||
<th class="text-right">План (тенге)</th>
|
||
<th class="text-right">Факт (тенге)</th>
|
||
<th class="text-right">Нарастающий итог</th>
|
||
<th class="text-center">Выполнение</th>
|
||
</tr>
|
||
</thead>
|
||
<tbody id="article-body"></tbody>
|
||
</table>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<script>
|
||
const articles = [
|
||
{code: '2.4.5', name: '2.4.5 - Командировочные расходы по подготовке кадров и повышению квалификации кадров'},
|
||
{code: '2.4.6', name: '2.4.6 - Подготовка и повышение квалификации кадров'},
|
||
{code: '4.1.6', name: '4.1.6 - Командировочные расходы по подготовке кадров и повышению квалификации кадров'},
|
||
{code: '4.1.7', name: '4.1.7 - Подготовка и повышение квалификации кадров'}
|
||
];
|
||
|
||
const branches = [
|
||
"Астана","Алматы","Шымкент","Атырау","Актобе","Караганда","Павлодар",
|
||
"Петропавловск","Костанай","Туркестан","Усть-Каменогорск","Уральск",
|
||
"Семей","Кызылорда","Тараз","Жезказган","Экибастуз","Темиртау",
|
||
"Балхаш","Турксиб","ЦА (Центральный аппарат)"
|
||
];
|
||
|
||
let budgetData = JSON.parse(localStorage.getItem('budget-data')) || [];
|
||
|
||
function saveData() {
|
||
localStorage.setItem('budget-data', JSON.stringify(budgetData));
|
||
}
|
||
|
||
function addTestData() {
|
||
budgetData = [];
|
||
branches.forEach(branch => {
|
||
articles.forEach(article => {
|
||
const plan = Math.floor(Math.random() * 5000000) + 1000000;
|
||
const fact = Math.floor(plan * (0.5 + Math.random() * 0.6));
|
||
budgetData.push({
|
||
branch: branch,
|
||
month: 'Июнь',
|
||
year: '2026',
|
||
article: article.name,
|
||
articleCode: article.code,
|
||
plan: plan,
|
||
fact: fact,
|
||
cumulative: fact
|
||
});
|
||
});
|
||
});
|
||
saveData();
|
||
renderAll();
|
||
}
|
||
|
||
function renderAll() {
|
||
renderSummary();
|
||
renderBranches();
|
||
renderArticles();
|
||
updateStats();
|
||
}
|
||
|
||
function updateStats() {
|
||
let totalPlan = 0, totalFact = 0;
|
||
budgetData.forEach(item => {
|
||
totalPlan += item.plan;
|
||
totalFact += item.fact;
|
||
});
|
||
|
||
const percent = totalPlan > 0 ? Math.round((totalFact / totalPlan) * 100) : 0;
|
||
const remain = totalPlan - totalFact;
|
||
|
||
document.getElementById('total-plan').textContent = totalPlan.toLocaleString('ru-RU');
|
||
document.getElementById('total-fact').textContent = totalFact.toLocaleString('ru-RU');
|
||
document.getElementById('total-percent').textContent = percent + '%';
|
||
document.getElementById('total-remain').textContent = remain.toLocaleString('ru-RU');
|
||
|
||
const uniqueBranches = [...new Set(budgetData.map(d => d.branch))];
|
||
document.getElementById('submitted-branches').textContent = uniqueBranches.length;
|
||
}
|
||
|
||
function renderSummary() {
|
||
const monthFilter = document.getElementById('month-filter').value;
|
||
const articleFilter = document.getElementById('article-filter').value;
|
||
|
||
let filtered = budgetData;
|
||
if (monthFilter !== 'all') filtered = filtered.filter(d => d.month === monthFilter);
|
||
if (articleFilter !== 'all') filtered = filtered.filter(d => d.articleCode === articleFilter);
|
||
|
||
const summary = {};
|
||
filtered.forEach(item => {
|
||
if (!summary[item.articleCode]) {
|
||
summary[item.articleCode] = {article: item.article, plan: 0, fact: 0, cumulative: 0};
|
||
}
|
||
summary[item.articleCode].plan += item.plan;
|
||
summary[item.articleCode].fact += item.fact;
|
||
summary[item.articleCode].cumulative += item.cumulative;
|
||
});
|
||
|
||
const tbody = document.getElementById('summary-body');
|
||
tbody.innerHTML = '';
|
||
|
||
let totalPlan = 0, totalFact = 0, totalCum = 0;
|
||
|
||
Object.values(summary).forEach(item => {
|
||
const percent = item.plan > 0 ? Math.round((item.fact / item.plan) * 100) : 0;
|
||
const progressClass = percent >= 90 ? 'good' : percent >= 70 ? 'warning' : 'danger';
|
||
|
||
totalPlan += item.plan;
|
||
totalFact += item.fact;
|
||
totalCum += item.cumulative;
|
||
|
||
const row = document.createElement('tr');
|
||
row.innerHTML = `
|
||
<td>${item.article}</td>
|
||
<td class="text-right">${item.plan.toLocaleString('ru-RU')}</td>
|
||
<td class="text-right">${item.fact.toLocaleString('ru-RU')}</td>
|
||
<td class="text-right">${item.cumulative.toLocaleString('ru-RU')}</td>
|
||
<td class="text-center">${percent}%</td>
|
||
<td><div class="progress-bar"><div class="progress-fill ${progressClass}" style="width:${percent}%"></div></div></td>
|
||
`;
|
||
tbody.appendChild(row);
|
||
});
|
||
|
||
const totalPercent = totalPlan > 0 ? Math.round((totalFact / totalPlan) * 100) : 0;
|
||
document.getElementById('foot-plan').textContent = totalPlan.toLocaleString('ru-RU');
|
||
document.getElementById('foot-fact').textContent = totalFact.toLocaleString('ru-RU');
|
||
document.getElementById('foot-cumulative').textContent = totalCum.toLocaleString('ru-RU');
|
||
document.getElementById('foot-percent').textContent = totalPercent + '%';
|
||
}
|
||
|
||
function renderBranches() {
|
||
const summary = {};
|
||
budgetData.forEach(item => {
|
||
if (!summary[item.branch]) {
|
||
summary[item.branch] = {plan: 0, fact: 0};
|
||
}
|
||
summary[item.branch].plan += item.plan;
|
||
summary[item.branch].fact += item.fact;
|
||
});
|
||
|
||
const tbody = document.getElementById('branch-body');
|
||
tbody.innerHTML = '';
|
||
|
||
Object.entries(summary).forEach(([branch, data]) => {
|
||
const percent = data.plan > 0 ? Math.round((data.fact / data.plan) * 100) : 0;
|
||
const progressClass = percent >= 90 ? 'good' : percent >= 70 ? 'warning' : 'danger';
|
||
const status = percent >= 90 ? 'В норме' : percent >= 70 ? 'Внимание' : 'Критично';
|
||
const statusClass = percent >= 90 ? 'good' : percent >= 70 ? 'warning' : 'danger';
|
||
|
||
const row = document.createElement('tr');
|
||
row.innerHTML = `
|
||
<td><strong>${branch}</strong></td>
|
||
<td class="text-right">${data.plan.toLocaleString('ru-RU')}</td>
|
||
<td class="text-right">${data.fact.toLocaleString('ru-RU')}</td>
|
||
<td class="text-center">${percent}%</td>
|
||
<td><div class="progress-bar"><div class="progress-fill ${progressClass}" style="width:${percent}%"></div></div></td>
|
||
<td><span class="btn btn-${statusClass}" style="font-size:12px">${status}</span></td>
|
||
`;
|
||
tbody.appendChild(row);
|
||
});
|
||
}
|
||
|
||
function renderArticles() {
|
||
const filter = document.getElementById('article-detail-filter').value;
|
||
let filtered = budgetData;
|
||
if (filter !== 'all') filtered = filtered.filter(d => d.articleCode === filter);
|
||
|
||
const tbody = document.getElementById('article-body');
|
||
tbody.innerHTML = '';
|
||
|
||
filtered.forEach(item => {
|
||
const percent = item.plan > 0 ? Math.round((item.fact / item.plan) * 100) : 0;
|
||
|
||
const row = document.createElement('tr');
|
||
row.innerHTML = `
|
||
<td>${item.article}</td>
|
||
<td>${item.branch}</td>
|
||
<td class="text-right">${item.plan.toLocaleString('ru-RU')}</td>
|
||
<td class="text-right">${item.fact.toLocaleString('ru-RU')}</td>
|
||
<td class="text-right">${item.cumulative.toLocaleString('ru-RU')}</td>
|
||
<td class="text-center">${percent}%</td>
|
||
`;
|
||
tbody.appendChild(row);
|
||
});
|
||
}
|
||
|
||
function filterData() {
|
||
renderSummary();
|
||
}
|
||
|
||
function filterBranchTable() {
|
||
const search = document.getElementById('search-branch').value.toLowerCase();
|
||
document.querySelectorAll('#branch-body tr').forEach(row => {
|
||
const text = row.textContent.toLowerCase();
|
||
row.style.display = text.includes(search) ? '' : 'none';
|
||
});
|
||
}
|
||
|
||
function filterArticleTable() {
|
||
renderArticles();
|
||
}
|
||
|
||
function showTab(tab) {
|
||
document.querySelectorAll('.tab').forEach(t => t.classList.remove('active'));
|
||
event.target.classList.add('active');
|
||
document.getElementById('tab-summary').classList.toggle('hidden', tab !== 'summary');
|
||
document.getElementById('tab-by-branch').classList.toggle('hidden', tab !== 'by-branch');
|
||
document.getElementById('tab-by-article').classList.toggle('hidden', tab !== 'by-article');
|
||
}
|
||
|
||
function exportToExcel() {
|
||
let csv = '\uFEFF';
|
||
csv += 'Статья;План;Факт;Нарастающий итог;Выполнение (%)\n';
|
||
|
||
const monthFilter = document.getElementById('month-filter').value;
|
||
const articleFilter = document.getElementById('article-filter').value;
|
||
|
||
let filtered = budgetData;
|
||
if (monthFilter !== 'all') filtered = filtered.filter(d => d.month === monthFilter);
|
||
if (articleFilter !== 'all') filtered = filtered.filter(d => d.articleCode === articleFilter);
|
||
|
||
const summary = {};
|
||
filtered.forEach(item => {
|
||
if (!summary[item.articleCode]) {
|
||
summary[item.articleCode] = {article: item.article, plan: 0, fact: 0, cumulative: 0};
|
||
}
|
||
summary[item.articleCode].plan += item.plan;
|
||
summary[item.articleCode].fact += item.fact;
|
||
summary[item.articleCode].cumulative += item.cumulative;
|
||
});
|
||
|
||
let totalPlan = 0, totalFact = 0;
|
||
Object.values(summary).forEach(item => {
|
||
const percent = item.plan > 0 ? Math.round((item.fact / item.plan) * 100) : 0;
|
||
csv += `${item.article};${item.plan};${item.fact};${item.cumulative};${percent}\n`;
|
||
totalPlan += item.plan;
|
||
totalFact += item.fact;
|
||
});
|
||
|
||
const totalPercent = totalPlan > 0 ? Math.round((totalFact / totalPlan) * 100) : 0;
|
||
csv += `ИТОГО;${totalPlan};${totalFact};${totalFact};${totalPercent}\n`;
|
||
|
||
downloadCSV(csv, `budget_summary_${new Date().toISOString().slice(0,10)}.csv`);
|
||
}
|
||
|
||
function exportBranchesToExcel() {
|
||
let csv = '\uFEFF';
|
||
csv += 'Филиал;План;Факт;Выполнение (%);Статус\n';
|
||
|
||
const summary = {};
|
||
budgetData.forEach(item => {
|
||
if (!summary[item.branch]) {
|
||
summary[item.branch] = {plan: 0, fact: 0};
|
||
}
|
||
summary[item.branch].plan += item.plan;
|
||
summary[item.branch].fact += item.fact;
|
||
});
|
||
|
||
Object.entries(summary).forEach(([branch, data]) => {
|
||
const percent = data.plan > 0 ? Math.round((data.fact / data.plan) * 100) : 0;
|
||
const status = percent >= 90 ? 'В норме' : percent >= 70 ? 'Внимание' : 'Критично';
|
||
csv += `${branch};${data.plan};${data.fact};${percent};${status}\n`;
|
||
});
|
||
|
||
downloadCSV(csv, `budget_by_branch_${new Date().toISOString().slice(0,10)}.csv`);
|
||
}
|
||
|
||
function exportArticlesToExcel() {
|
||
let csv = '\uFEFF';
|
||
csv += 'Статья;Филиал;План;Факт;Нарастающий итог;Выполнение (%)\n';
|
||
|
||
const filter = document.getElementById('article-detail-filter').value;
|
||
let filtered = budgetData;
|
||
if (filter !== 'all') filtered = filtered.filter(d => d.articleCode === filter);
|
||
|
||
filtered.forEach(item => {
|
||
const percent = item.plan > 0 ? Math.round((item.fact / item.plan) * 100) : 0;
|
||
csv += `${item.article};${item.branch};${item.plan};${item.fact};${item.cumulative};${percent}\n`;
|
||
});
|
||
|
||
downloadCSV(csv, `budget_by_article_${new Date().toISOString().slice(0,10)}.csv`);
|
||
}
|
||
|
||
function downloadCSV(csv, filename) {
|
||
const blob = new Blob([csv], { type: 'text/csv;charset=utf-8;' });
|
||
const link = document.createElement('a');
|
||
link.href = URL.createObjectURL(blob);
|
||
link.download = filename;
|
||
link.click();
|
||
}
|
||
|
||
renderAll();
|
||
</script>
|
||
|
||
</body>
|
||
</html>
|