analitika-toplivnyh-talonov/index.html

490 lines
19 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" data-theme="auto">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width,initial-scale=1">
<title>Аналитика топливных талонов</title>
<link rel="stylesheet" href="design-system/kt-ai-fonts.css">
<link rel="stylesheet" href="design-system/kt-ai-tokens.css">
<link rel="stylesheet" href="design-system/kt-ai-components.css">
<link rel="stylesheet" href="design-system/kt-ai-page.css">
<style>
a.kt-ai-btn { text-decoration: none; }
.stats-grid { display: grid; grid-template-columns: repeat(auto-fit, minmax(200px, 1fr)); gap: var(--kt-ai-space-6); margin-bottom: var(--kt-ai-space-9); }
.stat-card { background: var(--kt-ai-card-bg); border: 1px solid var(--kt-ai-card-border); border-radius: var(--kt-ai-radius-xl); padding: var(--kt-ai-space-6); }
.stat-value { font-size: var(--kt-ai-text-3xl); font-weight: var(--kt-ai-weight-bold); color: var(--kt-ai-primary); }
.stat-label { color: var(--kt-ai-fg-muted); margin-top: var(--kt-ai-space-2); }
.tabs { display: flex; gap: var(--kt-ai-space-2); margin-bottom: var(--kt-ai-space-6); }
.tab { padding: var(--kt-ai-space-3) var(--kt-ai-space-5); border-radius: var(--kt-ai-radius-lg); cursor: pointer; border: 1px solid transparent; }
.tab.active { background: var(--kt-ai-primary); color: white; }
.tab:hover:not(.active) { background: var(--kt-ai-bg-soft); }
.form-grid { display: grid; grid-template-columns: repeat(auto-fit, minmax(200px, 1fr)); gap: var(--kt-ai-space-4); }
.table-wrap { overflow-x: auto; }
table { width: 100%; border-collapse: collapse; }
th, td { padding: var(--kt-ai-space-3); text-align: left; border-bottom: 1px solid var(--kt-ai-divider); }
th { background: var(--kt-ai-bg-soft); font-weight: var(--kt-ai-weight-semibold); }
tr:hover { background: var(--kt-ai-bg-soft); }
.actions { display: flex; gap: var(--kt-ai-space-2); }
.search-row { display: flex; gap: var(--kt-ai-space-3); margin-bottom: var(--kt-ai-space-4); align-items: center; }
.search-input { flex: 1; }
.chart-placeholder { background: var(--kt-ai-bg-soft); border-radius: var(--kt-ai-radius-xl); padding: var(--kt-ai-space-6); text-align: center; color: var(--kt-ai-fg-muted); }
</style>
</head>
<body>
<header class="kt-ai-pagehead">
<div class="kt-ai-wrap kt-ai-pagehead-in">
<div style="display:flex;align-items:center;gap:var(--kt-ai-space-3)">
<img src="design-system/brand/kt-ai-logo.png" alt="Logo" height="24">
<strong>Аналитика топливных талонов</strong>
</div>
<span class="kt-ai-spacer"></span>
<a class="kt-ai-btn" data-variant="ghost" href="#" onclick="showTab('table');return false;">Талоны</a>
<a class="kt-ai-btn" data-variant="ghost" href="#" onclick="showTab('stats');return false;">Аналитика</a>
<a class="kt-ai-btn" data-variant="ghost" href="#" onclick="showTab('log');return false;">Журнал</a>
</div>
</header>
<section class="kt-ai-wrap" style="padding-top:var(--kt-ai-space-9)">
<div class="tabs">
<div class="tab active" id="tab-table" onclick="showTab('table')">Талоны</div>
<div class="tab" id="tab-stats" onclick="showTab('stats')">Аналитика</div>
<div class="tab" id="tab-log" onclick="showTab('log')">Журнал операций</div>
</div>
<div id="view-table">
<div class="kt-ai-card" style="margin-bottom:var(--kt-ai-space-6)">
<h2 style="margin-bottom:var(--kt-ai-space-4)">Добавить талон</h2>
<form id="fuelForm" onsubmit="submitForm(event)">
<input type="hidden" id="cardId">
<div class="form-grid">
<div class="kt-ai-field">
<label>Дата талона *</label>
<input type="date" class="kt-ai-input" id="date" required>
</div>
<div class="kt-ai-field">
<label>Дата фактической заправки *</label>
<input type="date" class="kt-ai-input" id="fuelDate" required>
</div>
<div class="kt-ai-field">
<label>Водитель *</label>
<input type="text" class="kt-ai-input" id="driver" placeholder="ФИО водителя" required>
</div>
<div class="kt-ai-field">
<label>Автомобиль *</label>
<input type="text" class="kt-ai-input" id="vehicle" placeholder="Марка, госномер" required>
</div>
<div class="kt-ai-field">
<label>Литраж *</label>
<input type="number" step="0.01" class="kt-ai-input" id="liters" placeholder="0.00" required>
</div>
<div class="kt-ai-field">
<label>Сумма (₸) *</label>
<input type="number" step="0.01" class="kt-ai-input" id="amount" placeholder="0.00" required>
</div>
<div class="kt-ai-field">
<label>АЗС</label>
<input type="text" class="kt-ai-input" id="gasStation" placeholder="Название АЗС">
</div>
<div class="kt-ai-field">
<label>Пробег (км)</label>
<input type="number" class="kt-ai-input" id="mileage" placeholder="0">
</div>
<div class="kt-ai-field">
<label>Статус</label>
<select class="kt-ai-select" id="status">
<option value="использован">Использован</option>
<option value="в работе">В работе</option>
<option value="потерян">Потерян</option>
</select>
</div>
</div>
<div class="kt-ai-field" style="margin-top:var(--kt-ai-space-4)">
<label>Примечание</label>
<textarea class="kt-ai-textarea" id="note" rows="2" placeholder="Комментарий"></textarea>
</div>
<div style="margin-top:var(--kt-ai-space-5);display:flex;gap:var(--kt-ai-space-3)">
<button type="submit" class="kt-ai-btn" data-variant="primary">Сохранить</button>
<button type="button" class="kt-ai-btn" data-variant="ghost" onclick="resetForm()">Очистить</button>
</div>
</form>
</div>
<div class="kt-ai-card">
<div style="display:flex;justify-content:space-between;align-items:center;margin-bottom:var(--kt-ai-space-4);flex-wrap:wrap;gap:var(--kt-ai-space-3)">
<h2>Все талоны</h2>
<div style="display:flex;gap:var(--kt-ai-space-2);flex-wrap:wrap">
<button class="kt-ai-btn" data-variant="ghost" onclick="exportToExcel()">Выгрузить в Excel</button>
<button class="kt-ai-btn" data-variant="ghost" onclick="loadFuelCards()">Обновить</button>
</div>
</div>
<div class="search-row">
<input type="text" class="kt-ai-input search-input" id="searchInput" placeholder="Поиск по водителю, авто, АЗС..." oninput="filterTable()">
<select class="kt-ai-select" id="filterStatus" onchange="filterTable()">
<option value="">Все статусы</option>
<option value="использован">Использован</option>
<option value="в работе">В работе</option>
<option value="потерян">Потерян</option>
</select>
</div>
<div class="table-wrap">
<table id="fuelTable">
<thead>
<tr>
<th>Дата талона</th>
<th>Дата заправки</th>
<th>Водитель</th>
<th>Автомобиль</th>
<th>Литры</th>
<th>Сумма</th>
<th>АЗС</th>
<th>Пробег</th>
<th>Статус</th>
<th>Действия</th>
</tr>
</thead>
<tbody id="fuelTableBody"></tbody>
</table>
</div>
</div>
</div>
<div id="view-stats" style="display:none">
<div class="stats-grid">
<div class="stat-card">
<div class="stat-value" id="statTotalCards">0</div>
<div class="stat-label">Всего талонов</div>
</div>
<div class="stat-card">
<div class="stat-value" id="statTotalLiters">0</div>
<div class="stat-label">Общий литраж (л)</div>
</div>
<div class="stat-card">
<div class="stat-value" id="statTotalAmount">0 ₸</div>
<div class="stat-label">Общая сумма</div>
</div>
<div class="stat-card">
<div class="stat-value" id="statAvgPrice">0 ₸/л</div>
<div class="stat-label">Средняя цена/л</div>
</div>
</div>
<div class="kt-ai-card" style="margin-bottom:var(--kt-ai-space-6)">
<h3 style="margin-bottom:var(--kt-ai-space-4)">По водителям</h3>
<div class="table-wrap">
<table id="byDriverTable">
<thead>
<tr>
<th>Водитель</th>
<th>Кол-во талонов</th>
<th>Литры</th>
<th>Сумма (₸)</th>
</tr>
</thead>
<tbody></tbody>
</table>
</div>
</div>
<div class="kt-ai-card" style="margin-bottom:var(--kt-ai-space-6)">
<h3 style="margin-bottom:var(--kt-ai-space-4)">По автомобилям</h3>
<div class="table-wrap">
<table id="byVehicleTable">
<thead>
<tr>
<th>Автомобиль</th>
<th>Кол-во талонов</th>
<th>Литры</th>
<th>Сумма (₸)</th>
</tr>
</thead>
<tbody></tbody>
</table>
</div>
</div>
<div class="kt-ai-card">
<h3 style="margin-bottom:var(--kt-ai-space-4)">По АЗС</h3>
<div class="table-wrap">
<table id="byGasStationTable">
<thead>
<tr>
<th>АЗС</th>
<th>Кол-во талонов</th>
<th>Литры</th>
<th>Сумма (₸)</th>
</tr>
</thead>
<tbody></tbody>
</table>
</div>
</div>
</div>
<div id="view-log" style="display:none">
<div class="kt-ai-card">
<div style="display:flex;justify-content:space-between;align-items:center;margin-bottom:var(--kt-ai-space-4)">
<h2>Журнал операций</h2>
<button class="kt-ai-btn" data-variant="ghost" onclick="loadLog()">Обновить</button>
</div>
<div class="table-wrap">
<table id="logTable">
<thead>
<tr>
<th>Время</th>
<th>Действие</th>
<th>Пользователь</th>
</tr>
</thead>
<tbody id="logTableBody"></tbody>
</table>
</div>
</div>
</div>
</section>
<footer class="kt-ai-wrap kt-ai-pagefoot" style="margin-top:var(--kt-ai-space-12)">
<span>© 2026 Аналитика топливных талонов</span>
<span class="kt-ai-spacer"></span>
<span>Учёт и контроль расхода ГСМ</span>
</footer>
<script>
let allCards = [];
function showTab(name) {
document.querySelectorAll('.tab').forEach(t => t.classList.remove('active'));
document.getElementById('tab-' + name).classList.add('active');
document.getElementById('view-table').style.display = name === 'table' ? 'block' : 'none';
document.getElementById('view-stats').style.display = name === 'stats' ? 'block' : 'none';
document.getElementById('view-log').style.display = name === 'log' ? 'block' : 'none';
if (name === 'stats') loadStats();
if (name === 'log') loadLog();
}
document.getElementById('date').valueAsDate = new Date();
document.getElementById('fuelDate').valueAsDate = new Date();
async function loadFuelCards() {
try {
const res = await fetch('api/fuel-cards');
allCards = await res.json();
renderTable(allCards);
} catch (err) {
console.error('Ошибка загрузки:', err);
}
}
function renderTable(cards) {
const tbody = document.getElementById('fuelTableBody');
tbody.innerHTML = '';
cards.forEach(card => {
const tr = document.createElement('tr');
const statusClass = card.status === 'потерян' ? 'data-variant="danger"' : '';
tr.innerHTML =
'<td>' + formatDate(card.date) + '</td>' +
'<td>' + formatDate(card.fuelDate) + '</td>' +
'<td>' + escapeHtml(card.driver) + '</td>' +
'<td>' + escapeHtml(card.vehicle) + '</td>' +
'<td>' + parseFloat(card.liters).toFixed(2) + '</td>' +
'<td>' + parseFloat(card.amount).toFixed(2) + ' ₸</td>' +
'<td>' + escapeHtml(card.gasStation || '-') + '</td>' +
'<td>' + (card.mileage ? card.mileage : '-') + '</td>' +
'<td><span class="kt-ai-chip" ' + statusClass + '>' + escapeHtml(card.status) + '</span></td>' +
'<td class="actions">' +
'<button class="kt-ai-btn" data-variant="ghost" onclick="editCard(' + card.id + ')">✏️</button>' +
'<button class="kt-ai-btn" data-variant="ghost" onclick="deleteCard(' + card.id + ')">🗑️</button>' +
'</td>';
tbody.appendChild(tr);
});
}
function formatDate(dateStr) {
if (!dateStr) return '-';
const d = new Date(dateStr);
const day = String(d.getDate()).padStart(2, '0');
const month = String(d.getMonth() + 1).padStart(2, '0');
const year = d.getFullYear();
return day + '.' + month + '.' + year;
}
function escapeHtml(text) {
if (!text) return '';
return String(text).replace(/&/g, '&amp;').replace(/</g, '&lt;').replace(/>/g, '&gt;').replace(/"/g, '&quot;');
}
function filterTable() {
const search = document.getElementById('searchInput').value.toLowerCase();
const status = document.getElementById('filterStatus').value;
const filtered = allCards.filter(card => {
const matchSearch = !search ||
(card.driver && card.driver.toLowerCase().includes(search)) ||
(card.vehicle && card.vehicle.toLowerCase().includes(search)) ||
(card.gasStation && card.gasStation.toLowerCase().includes(search));
const matchStatus = !status || card.status === status;
return matchSearch && matchStatus;
});
renderTable(filtered);
}
async function submitForm(e) {
e.preventDefault();
const id = document.getElementById('cardId').value;
const card = {
date: document.getElementById('date').value,
fuelDate: document.getElementById('fuelDate').value,
driver: document.getElementById('driver').value,
vehicle: document.getElementById('vehicle').value,
liters: document.getElementById('liters').value,
amount: document.getElementById('amount').value,
gasStation: document.getElementById('gasStation').value,
mileage: document.getElementById('mileage').value,
status: document.getElementById('status').value,
note: document.getElementById('note').value
};
try {
const url = id ? 'api/fuel-cards' : 'api/fuel-cards';
const method = id ? 'PUT' : 'POST';
if (id) card.id = parseInt(id);
const res = await fetch(url, {
method: method,
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(card)
});
if (res.ok) {
resetForm();
loadFuelCards();
alert(id ? 'Талон обновлён' : 'Талон добавлен');
} else {
alert('Ошибка сохранения');
}
} catch (err) {
console.error(err);
alert('Ошибка: ' + err.message);
}
}
function resetForm() {
document.getElementById('cardId').value = '';
document.getElementById('fuelForm').reset();
document.getElementById('date').valueAsDate = new Date();
document.getElementById('fuelDate').valueAsDate = new Date();
}
async function editCard(id) {
const card = allCards.find(c => c.id === id);
if (!card) return;
document.getElementById('cardId').value = card.id;
document.getElementById('date').value = card.date;
document.getElementById('fuelDate').value = card.fuelDate;
document.getElementById('driver').value = card.driver;
document.getElementById('vehicle').value = card.vehicle;
document.getElementById('liters').value = card.liters;
document.getElementById('amount').value = card.amount;
document.getElementById('gasStation').value = card.gasStation || '';
document.getElementById('mileage').value = card.mileage || '';
document.getElementById('status').value = card.status;
document.getElementById('note').value = card.note || '';
window.scrollTo({ top: 0, behavior: 'smooth' });
}
async function deleteCard(id) {
if (!confirm('Удалить этот талон?')) return;
try {
const res = await fetch('api/fuel-cards/' + id, { method: 'DELETE' });
if (res.ok) {
loadFuelCards();
} else {
alert('Ошибка удаления');
}
} catch (err) {
alert('Ошибка: ' + err.message);
}
}
async function loadStats() {
try {
const res = await fetch('api/stats');
const stats = await res.json();
document.getElementById('statTotalCards').textContent = stats.totalCards;
document.getElementById('statTotalLiters').textContent = parseFloat(stats.totalLiters).toFixed(2);
document.getElementById('statTotalAmount').textContent = parseFloat(stats.totalAmount).toFixed(2) + ' ₸';
document.getElementById('statAvgPrice').textContent = parseFloat(stats.avgPricePerLiter).toFixed(2) + ' ₸/л';
renderStatsTable('byDriverTable', stats.byDriver);
renderStatsTable('byVehicleTable', stats.byVehicle);
renderStatsTable('byGasStationTable', stats.byGasStation);
} catch (err) {
console.error('Ошибка загрузки статистики:', err);
}
}
function renderStatsTable(tableId, data) {
const tbody = document.querySelector('#' + tableId + ' tbody');
tbody.innerHTML = '';
Object.entries(data).forEach(([name, vals]) => {
const tr = document.createElement('tr');
tr.innerHTML =
'<td>' + escapeHtml(name) + '</td>' +
'<td>' + vals.count + '</td>' +
'<td>' + parseFloat(vals.liters).toFixed(2) + '</td>' +
'<td>' + parseFloat(vals.amount).toFixed(2) + '</td>';
tbody.appendChild(tr);
});
}
async function loadLog() {
try {
const res = await fetch('api/log');
const logs = await res.json();
const tbody = document.getElementById('logTableBody');
tbody.innerHTML = '';
logs.forEach(log => {
const tr = document.createElement('tr');
const time = new Date(log.when).toLocaleString('ru-RU');
tr.innerHTML =
'<td>' + time + '</td>' +
'<td>' + escapeHtml(log.what) + '</td>' +
'<td>' + escapeHtml(log.who) + '</td>';
tbody.appendChild(tr);
});
} catch (err) {
console.error('Ошибка загрузки журнала:', err);
}
}
function exportToExcel() {
const head = ["Дата талона", "Дата заправки", "Водитель", "Автомобиль", "Литры", "Сумма", "АЗС", "Пробег", "Статус", "Примечание"];
const body = allCards.map(r => [
r.date,
r.fuelDate,
r.driver,
r.vehicle,
r.liters,
r.amount,
r.gasStation || '',
r.mileage || '',
r.status,
r.note || ''
].map(v => '"' + String(v ?? "").replace(/"/g, '""') + '"').join(";"));
const csv = "\uFEFF" + [head.join(";"), ...body].join("\n");
const a = document.createElement("a");
a.href = URL.createObjectURL(new Blob([csv], { type: "text/csv" }));
a.download = "toplivnye-talony-" + new Date().toISOString().slice(0,10) + ".csv";
a.click();
}
loadFuelCards();
</script>
</body>
</html>