v2 - dashboard for tracking submissions + reminders
This commit is contained in:
parent
e035bd72ce
commit
ac2246a784
325
dashboard.html
Normal file
325
dashboard.html
Normal file
@ -0,0 +1,325 @@
|
|||||||
|
<!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:1200px;margin:0 auto;padding:32px 24px}
|
||||||
|
.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)}
|
||||||
|
.stats{display:grid;grid-template-columns:repeat(4,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)}
|
||||||
|
th{font-weight:600;color:var(--gray-500);font-size:13px;text-transform:uppercase}
|
||||||
|
tr:hover{background:var(--cyan-50)}
|
||||||
|
.status{display:inline-block;padding:4px 12px;border-radius:20px;font-size:12px;font-weight:600}
|
||||||
|
.status.done{background:#D1FAE5;color:#065F46}
|
||||||
|
.status.pending{background:#FEF3C7;color:#92400E}
|
||||||
|
.status.overdue{background:#FEE2E2;color:#991B1B}
|
||||||
|
.btn{display:inline-block;padding:8px 16px;border-radius:6px;font-weight:600;font-size:13px;text-decoration:none;cursor:pointer;border:none}
|
||||||
|
.btn-remind{background:var(--cyan);color:var(--ink)}
|
||||||
|
.btn-remind:hover{background:#1be5ff}
|
||||||
|
.btn-download{background:var(--gray-100);color:var(--ink)}
|
||||||
|
.filter-bar{display:flex;gap:12px;margin-bottom:16px;flex-wrap:wrap}
|
||||||
|
.filter-bar input,.filter-bar select{padding:8px 12px;border:2px solid var(--gray-100);border-radius:6px;font-size:14px}
|
||||||
|
.hidden{display:none}
|
||||||
|
@media(max-width:768px){
|
||||||
|
.stats{grid-template-columns:repeat(2,1fr)}
|
||||||
|
table{font-size:13px}
|
||||||
|
th,td{padding:8px 12px}
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
|
||||||
|
<div class="header">
|
||||||
|
<h1>Дашборд сбора данных ЕППК</h1>
|
||||||
|
<p>Отслеживание сдачи отчётов от филиалов Казахтелеком</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="container">
|
||||||
|
<div class="tabs">
|
||||||
|
<button class="tab active" onclick="showTab('budget')">Бюджет (ежемесячно)</button>
|
||||||
|
<button class="tab" onclick="showTab('eppk')">ЕППК (годовой)</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Бюджет -->
|
||||||
|
<div id="tab-budget">
|
||||||
|
<div class="stats">
|
||||||
|
<div class="stat-card">
|
||||||
|
<div class="label">Всего филиалов</div>
|
||||||
|
<div class="value" id="total-branches">21</div>
|
||||||
|
</div>
|
||||||
|
<div class="stat-card">
|
||||||
|
<div class="label">Сдали отчёт</div>
|
||||||
|
<div class="value green" id="submitted-count">0</div>
|
||||||
|
</div>
|
||||||
|
<div class="stat-card">
|
||||||
|
<div class="label">Ожидают</div>
|
||||||
|
<div class="value yellow" id="pending-count">0</div>
|
||||||
|
</div>
|
||||||
|
<div class="stat-card">
|
||||||
|
<div class="label">Просрочено</div>
|
||||||
|
<div class="value red" id="overdue-count">0</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="card">
|
||||||
|
<h2>Список филиалов — Бюджет обучений и командировок</h2>
|
||||||
|
<div class="filter-bar">
|
||||||
|
<input type="text" id="search-budget" placeholder="Поиск филиала..." oninput="filterTable('budget')">
|
||||||
|
<select id="status-filter-budget" onchange="filterTable('budget')">
|
||||||
|
<option value="all">Все статусы</option>
|
||||||
|
<option value="done">Сдали</option>
|
||||||
|
<option value="pending">Ожидают</option>
|
||||||
|
<option value="overdue">Просрочено</option>
|
||||||
|
</select>
|
||||||
|
<button class="btn btn-remind" onclick="remindAll('budget')">Напомнить всем</button>
|
||||||
|
<button class="btn btn-download" onclick="downloadTemplate()">Скачать шаблон Excel</button>
|
||||||
|
</div>
|
||||||
|
<table id="table-budget">
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th>Филиал</th>
|
||||||
|
<th>Статус</th>
|
||||||
|
<th>Дата сдачи</th>
|
||||||
|
<th>Период</th>
|
||||||
|
<th>Действия</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody id="tbody-budget"></tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- ЕППК -->
|
||||||
|
<div id="tab-eppk" class="hidden">
|
||||||
|
<div class="stats">
|
||||||
|
<div class="stat-card">
|
||||||
|
<div class="label">Всего филиалов</div>
|
||||||
|
<div class="value">21</div>
|
||||||
|
</div>
|
||||||
|
<div class="stat-card">
|
||||||
|
<div class="label">Сдали ЕППК</div>
|
||||||
|
<div class="value green" id="eppk-submitted">0</div>
|
||||||
|
</div>
|
||||||
|
<div class="stat-card">
|
||||||
|
<div class="label">Ожидают</div>
|
||||||
|
<div class="value yellow" id="eppk-pending">0</div>
|
||||||
|
</div>
|
||||||
|
<div class="stat-card">
|
||||||
|
<div class="label">Просрочено</div>
|
||||||
|
<div class="value red" id="eppk-overdue">0</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="card">
|
||||||
|
<h2>ЕППК — Единый план повышения квалификации</h2>
|
||||||
|
<div class="filter-bar">
|
||||||
|
<input type="text" id="search-eppk" placeholder="Поиск филиала..." oninput="filterTable('eppk')">
|
||||||
|
<select id="status-filter-eppk" onchange="filterTable('eppk')">
|
||||||
|
<option value="all">Все статусы</option>
|
||||||
|
<option value="done">Сдали</option>
|
||||||
|
<option value="pending">Ожидают</option>
|
||||||
|
<option value="overdue">Просрочено</option>
|
||||||
|
</select>
|
||||||
|
<button class="btn btn-remind" onclick="remindAll('eppk')">Напомнить всем</button>
|
||||||
|
</div>
|
||||||
|
<table id="table-eppk">
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th>Филиал</th>
|
||||||
|
<th>Статус</th>
|
||||||
|
<th>Дата сдачи</th>
|
||||||
|
<th>Год</th>
|
||||||
|
<th>Действия</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody id="tbody-eppk"></tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
const branches = [
|
||||||
|
"Астана","Алматы","Шымкент","Атырау","Актобе","Караганда","Павлодар",
|
||||||
|
"Петропавловск","Костанай","Туркестан","Усть-Каменогорск","Уральск",
|
||||||
|
"Семей","Кызылорда","Тараз","Жезказган","Экибастуз","Темиртау",
|
||||||
|
"Балхаш","Турксиб","ЦА (Центральный аппарат)"
|
||||||
|
];
|
||||||
|
|
||||||
|
const emails = {
|
||||||
|
"Астана":"astana@kaztelecom.kz",
|
||||||
|
"Алматы":"almaty@kaztelecom.kz",
|
||||||
|
"Шымкент":"shymkent@kaztelecom.kz",
|
||||||
|
"Атырау":"atyrau@kaztelecom.kz",
|
||||||
|
"Актобе":"aktobe@kaztelecom.kz",
|
||||||
|
"Караганда":"karaganda@kaztelecom.kz",
|
||||||
|
"Павлодар":"pavlodar@kaztelecom.kz",
|
||||||
|
"Петропавловск":"petropavlovsk@kaztelecom.kz",
|
||||||
|
"Костанай":"kostanay@kaztelecom.kz",
|
||||||
|
"Туркестан":"turkestan@kaztelecom.kz",
|
||||||
|
"Усть-Каменогорск":"uskamenogorsk@kaztelecom.kz",
|
||||||
|
"Уральск":"uralsk@kaztelecom.kz",
|
||||||
|
"Семей":"semey@kaztelecom.kz",
|
||||||
|
"Кызылорда":"kyzylorda@kaztelecom.kz",
|
||||||
|
"Тараз":"taraz@kaztelecom.kz",
|
||||||
|
"Жезказган":"jezkazgan@kaztelecom.kz",
|
||||||
|
"Экибастуз":"ekibastuz@kaztelecom.kz",
|
||||||
|
"Темиртау":"temirtau@kaztelecom.kz",
|
||||||
|
"Балхаш":"balhash@kaztelecom.kz",
|
||||||
|
"Турксиб":"turksib@kaztelecom.kz",
|
||||||
|
"ЦА (Центральный аппарат)":"ca@kaztelecom.kz"
|
||||||
|
};
|
||||||
|
|
||||||
|
let data = JSON.parse(localStorage.getItem('eppk-data')) || {};
|
||||||
|
|
||||||
|
function saveData() {
|
||||||
|
localStorage.setItem('eppk-data', JSON.stringify(data));
|
||||||
|
}
|
||||||
|
|
||||||
|
function getStatus(branch, type) {
|
||||||
|
const key = `${branch}-${type}`;
|
||||||
|
if (data[key]) return data[key];
|
||||||
|
return { status: 'pending', date: '' };
|
||||||
|
}
|
||||||
|
|
||||||
|
function setStatus(branch, type, status, date) {
|
||||||
|
const key = `${branch}-${type}`;
|
||||||
|
data[key] = { status, date };
|
||||||
|
saveData();
|
||||||
|
}
|
||||||
|
|
||||||
|
function renderTable(type) {
|
||||||
|
const tbody = document.getElementById(`tbody-${type}`);
|
||||||
|
tbody.innerHTML = '';
|
||||||
|
|
||||||
|
branches.forEach(branch => {
|
||||||
|
const info = getStatus(branch, type);
|
||||||
|
const statusClass = info.status;
|
||||||
|
const statusText = info.status === 'done' ? 'Сдал' : info.status === 'overdue' ? 'Просрочено' : 'Ожидает';
|
||||||
|
const period = type === 'budget' ? 'Текущий месяц' : '2026';
|
||||||
|
|
||||||
|
const row = document.createElement('tr');
|
||||||
|
row.setAttribute('data-status', info.status);
|
||||||
|
row.setAttribute('data-branch', branch.toLowerCase());
|
||||||
|
row.innerHTML = `
|
||||||
|
<td><strong>${branch}</strong></td>
|
||||||
|
<td><span class="status ${statusClass}">${statusText}</span></td>
|
||||||
|
<td>${info.date || '—'}</td>
|
||||||
|
<td>${period}</td>
|
||||||
|
<td>
|
||||||
|
<button class="btn btn-remind" onclick="sendReminder('${branch}','${type}')" style="margin-right:6px">Напомнить</button>
|
||||||
|
<button class="btn btn-download" onclick="markDone('${branch}','${type}')">Сдал ✓</button>
|
||||||
|
</td>
|
||||||
|
`;
|
||||||
|
tbody.appendChild(row);
|
||||||
|
});
|
||||||
|
|
||||||
|
updateStats(type);
|
||||||
|
}
|
||||||
|
|
||||||
|
function updateStats(type) {
|
||||||
|
let done = 0, pending = 0, overdue = 0;
|
||||||
|
branches.forEach(branch => {
|
||||||
|
const info = getStatus(branch, type);
|
||||||
|
if (info.status === 'done') done++;
|
||||||
|
else if (info.status === 'overdue') overdue++;
|
||||||
|
else pending++;
|
||||||
|
});
|
||||||
|
|
||||||
|
if (type === 'budget') {
|
||||||
|
document.getElementById('submitted-count').textContent = done;
|
||||||
|
document.getElementById('pending-count').textContent = pending;
|
||||||
|
document.getElementById('overdue-count').textContent = overdue;
|
||||||
|
} else {
|
||||||
|
document.getElementById('eppk-submitted').textContent = done;
|
||||||
|
document.getElementById('eppk-pending').textContent = pending;
|
||||||
|
document.getElementById('eppk-overdue').textContent = overdue;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function filterTable(type) {
|
||||||
|
const search = document.getElementById(`search-${type}`).value.toLowerCase();
|
||||||
|
const statusFilter = document.getElementById(`status-filter-${type}`).value;
|
||||||
|
const rows = document.querySelectorAll(`#tbody-${type} tr`);
|
||||||
|
|
||||||
|
rows.forEach(row => {
|
||||||
|
const branch = row.getAttribute('data-branch');
|
||||||
|
const status = row.getAttribute('data-status');
|
||||||
|
const matchSearch = branch.includes(search);
|
||||||
|
const matchStatus = statusFilter === 'all' || status === statusFilter;
|
||||||
|
row.style.display = matchSearch && matchStatus ? '' : 'none';
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function sendReminder(branch, type) {
|
||||||
|
const email = emails[branch] || '';
|
||||||
|
const subject = type === 'budget'
|
||||||
|
? 'Напоминание: сдача ежемесячного отчёта по бюджету обучений'
|
||||||
|
: 'Напоминание: сдача ЕППК — Единый план повышения квалификации';
|
||||||
|
const body = type === 'budget'
|
||||||
|
? `Уважаемые коллеги!\n\nНапоминаем о необходимости сдачи ежемесячного отчёта по бюджету обучений и командировочных расходов.\n\nПожалуйста, заполните форму: https://pages.git.vibe42.kz/1ospanovaroza/eppk-form/\n\nС уважением,\nЦентральный аппарат`
|
||||||
|
: `Уважаемые коллеги!\n\nНапоминаем о необходимости сдачи ЕППК — Единого плана повышения квалификации.\n\nПожалуйста, заполните форму: https://pages.git.vibe42.kz/1ospanovaroza/eppk-form/\n\nС уважением,\nЦентральный аппарат`;
|
||||||
|
window.open(`mailto:${email}?subject=${encodeURIComponent(subject)}&body=${encodeURIComponent(body)}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
function remindAll(type) {
|
||||||
|
let emails_list = branches.map(b => emails[b]).filter(e => e).join(',');
|
||||||
|
const subject = type === 'budget'
|
||||||
|
? 'Напоминание: сдача ежемесячного отчёта по бюджету обучений'
|
||||||
|
: 'Напоминание: сдача ЕППК';
|
||||||
|
window.open(`mailto:?bcc=${emails_list}&subject=${encodeURIComponent(subject)}&body=${encodeURIComponent('Уважаемые коллеги!\n\nПросим сдать отчёт в срок.\n\nФорма: https://pages.git.vibe42.kz/1ospanovaroza/eppk-form/')}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
function markDone(branch, type) {
|
||||||
|
const today = new Date().toLocaleDateString('ru-RU');
|
||||||
|
setStatus(branch, type, 'done', today);
|
||||||
|
renderTable(type);
|
||||||
|
}
|
||||||
|
|
||||||
|
function showTab(tab) {
|
||||||
|
document.querySelectorAll('.tab').forEach(t => t.classList.remove('active'));
|
||||||
|
event.target.classList.add('active');
|
||||||
|
document.getElementById('tab-budget').classList.toggle('hidden', tab !== 'budget');
|
||||||
|
document.getElementById('tab-eppk').classList.toggle('hidden', tab !== 'eppk');
|
||||||
|
}
|
||||||
|
|
||||||
|
function downloadTemplate() {
|
||||||
|
const headers = ['Филиал','Бюджет обучений (тенге)','Бюджет командировок (тенге)','Запланировано обучений','Прошло обучений','Комментарий'];
|
||||||
|
let csv = '\uFEFF' + headers.join(';') + '\n';
|
||||||
|
branches.forEach(b => {
|
||||||
|
csv += `${b};;;;;;;;;\n`;
|
||||||
|
});
|
||||||
|
const blob = new Blob([csv], { type: 'text/csv;charset=utf-8;' });
|
||||||
|
const link = document.createElement('a');
|
||||||
|
link.href = URL.createObjectURL(blob);
|
||||||
|
link.download = `shablon_eppk_${new Date().toISOString().slice(0,7)}.csv`;
|
||||||
|
link.click();
|
||||||
|
}
|
||||||
|
|
||||||
|
renderTable('budget');
|
||||||
|
renderTable('eppk');
|
||||||
|
</script>
|
||||||
|
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
Loading…
Reference in New Issue
Block a user