uchet-sluzhebnyh-avtomobiley/index.html

547 lines
26 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>
<style>
:root {
--bg: #0f1218;
--card: #1a1d26;
--card-hover: #222632;
--text: #f4f4f5;
--text-dim: #a1a1aa;
--accent: #00b8cc;
--accent-hover: #00d9e8;
--danger: #ef4444;
--success: #22c55e;
--warning: #f59e0b;
--info: #3b82f6;
--border: #2d3142;
}
* { margin: 0; box-sizing: border-box; }
body {
font-family: -apple-system, "Segoe UI", Roboto, sans-serif;
background: var(--bg);
color: var(--text);
min-height: 100vh;
padding: 24px;
}
.container { max-width: 1400px; margin: 0 auto; }
h1 { font-size: 28px; margin-bottom: 8px; }
.subtitle { color: var(--text-dim); margin-bottom: 24px; }
.stats { display: grid; grid-template-columns: repeat(auto-fit, minmax(140px, 1fr)); gap: 12px; margin-bottom: 24px; }
.stat { background: var(--card); padding: 16px; border-radius: 12px; text-align: center; }
.stat-value { font-size: 32px; font-weight: 700; color: var(--accent); }
.stat-label { font-size: 13px; color: var(--text-dim); margin-top: 4px; }
.stat.in-work .stat-value { color: var(--info); }
.stat.closed .stat-value { color: var(--success); }
.stat.overdue .stat-value { color: var(--danger); }
.stat.maintenance .stat-value { color: var(--warning); }
.controls { display: flex; gap: 12px; flex-wrap: wrap; margin-bottom: 24px; align-items: center; }
.search { flex: 1; min-width: 280px; padding: 12px 16px; background: var(--card); border: 1px solid var(--border); border-radius: 8px; color: var(--text); font-size: 14px; }
.search::placeholder { color: var(--text-dim); }
.btn { padding: 12px 20px; border: none; border-radius: 8px; cursor: pointer; font-size: 14px; font-weight: 500; transition: all 0.2s; }
.btn-primary { background: var(--accent); color: #000; }
.btn-primary:hover { background: var(--accent-hover); }
.btn-danger { background: var(--danger); color: #fff; }
.btn-secondary { background: var(--card); color: var(--text); border: 1px solid var(--border); }
.btn-secondary:hover { background: var(--card-hover); }
.chips { display: flex; gap: 8px; flex-wrap: wrap; margin-bottom: 24px; }
.chip { padding: 8px 16px; background: var(--card); border: 1px solid var(--border); border-radius: 20px; cursor: pointer; font-size: 13px; transition: all 0.2s; }
.chip:hover { background: var(--card-hover); }
.chip.active { background: var(--accent); color: #000; border-color: var(--accent); }
.table-container { background: var(--card); border-radius: 12px; overflow: hidden; margin-bottom: 24px; }
table { width: 100%; border-collapse: collapse; }
th, td { padding: 14px 16px; text-align: left; border-bottom: 1px solid var(--border); }
th { background: rgba(0,0,0,0.2); font-weight: 600; font-size: 13px; color: var(--text-dim); text-transform: uppercase; letter-spacing: 0.5px; }
tr { cursor: pointer; transition: background 0.2s; }
tr:hover { background: var(--card-hover); }
.status-badge { padding: 4px 10px; border-radius: 12px; font-size: 12px; font-weight: 500; }
.status-in-work { background: rgba(59, 130, 246, 0.2); color: #60a5fa; }
.status-closed { background: rgba(34, 197, 94, 0.2); color: #4ade80; }
.status-overdue { background: rgba(239, 68, 68, 0.2); color: #f87171; }
.status-maintenance { background: rgba(245, 158, 11, 0.2); color: #fbbf24; }
.modal-overlay { position: fixed; inset: 0; background: rgba(0,0,0,0.7); display: none; align-items: center; justify-content: center; z-index: 100; }
.modal-overlay.open { display: flex; }
.modal { background: var(--card); border-radius: 16px; padding: 32px; max-width: 680px; width: 90%; max-height: 90vh; overflow-y: auto; }
.modal h2 { margin-bottom: 24px; font-size: 22px; }
.form-grid { display: grid; grid-template-columns: 1fr 1fr; gap: 16px; }
.form-group { margin-bottom: 16px; }
.form-group.full { grid-column: 1 / -1; }
label { display: block; margin-bottom: 6px; font-size: 13px; color: var(--text-dim); }
input, select, textarea { width: 100%; padding: 10px 12px; background: var(--bg); border: 1px solid var(--border); border-radius: 6px; color: var(--text); font-size: 14px; }
input:focus, select:focus, textarea:focus { outline: none; border-color: var(--accent); }
.modal-actions { display: flex; gap: 12px; justify-content: flex-end; margin-top: 24px; }
.log-section { background: var(--card); border-radius: 12px; padding: 24px; margin-bottom: 24px; }
.log-section h3 { margin-bottom: 16px; font-size: 18px; }
.log-list { max-height: 200px; overflow-y: auto; }
.log-item { padding: 8px 0; border-bottom: 1px solid var(--border); font-size: 13px; color: var(--text-dim); }
.log-item:last-child { border-bottom: none; }
.log-time { color: var(--accent); margin-right: 8px; }
.confirm-overlay { position: fixed; inset: 0; background: rgba(0,0,0,0.7); display: none; align-items: center; justify-content: center; z-index: 200; }
.confirm-overlay.open { display: flex; }
.confirm { background: var(--card); border-radius: 16px; padding: 32px; max-width: 420px; width: 90%; text-align: center; }
.confirm h3 { margin-bottom: 12px; font-size: 20px; }
.confirm p { color: var(--text-dim); margin-bottom: 24px; }
.confirm-actions { display: flex; gap: 12px; justify-content: center; }
.empty { text-align: center; padding: 60px 20px; color: var(--text-dim); }
</style>
</head>
<body>
<div class="container">
<h1>Учёт служебных автомобилей</h1>
<p class="subtitle">Система учёта путевых листов автопарка</p>
<div class="stats" id="stats"></div>
<div class="controls">
<input type="text" class="search" id="search" placeholder="Поиск по водителю, автомобилю, маршруту или номеру путевого листа...">
<button class="btn btn-primary" id="btnAdd">Добавить путевой лист</button>
<button class="btn btn-secondary" id="btnExport">Выгрузить в CSV</button>
</div>
<div class="chips" id="chips"></div>
<div class="table-container">
<table>
<thead>
<tr>
<th>№ ПЛ</th>
<th>Дата</th>
<th>Автомобиль</th>
<th>Водитель</th>
<th>Маршрут</th>
<th>Пробег (км)</th>
<th>Статус</th>
</tr>
</thead>
<tbody id="tableBody"></tbody>
</table>
<div class="empty" id="empty" style="display:none">Нет записей</div>
</div>
<div class="log-section">
<h3>Журнал изменений</h3>
<div class="log-list" id="logList"></div>
</div>
</div>
<div class="modal-overlay" id="modalOverlay">
<div class="modal">
<h2 id="modalTitle">Путевой лист</h2>
<div class="form-grid">
<div class="form-group">
<label>Номер путевого листа</label>
<input type="text" id="fNumber" placeholder="ПЛ-2024-001">
</div>
<div class="form-group">
<label>Дата</label>
<input type="date" id="fDate">
</div>
<div class="form-group">
<label>Автомобиль</label>
<input type="text" id="fCar" placeholder="Toyota Camry">
</div>
<div class="form-group">
<label>Водитель</label>
<input type="text" id="fDriver" placeholder="ФИО">
</div>
<div class="form-group full">
<label>Маршрут</label>
<input type="text" id="fRoute" placeholder="Алматы - Астана">
</div>
<div class="form-group">
<label>Начальный километраж (км)</label>
<input type="number" id="fStartKm" placeholder="0">
</div>
<div class="form-group">
<label>Конечный километраж (км)</label>
<input type="number" id="fEndKm" placeholder="0">
</div>
<div class="form-group">
<label>Выдача топлива (л)</label>
<input type="number" id="fFuelGiven" placeholder="0">
</div>
<div class="form-group">
<label>Расход топлива (л)</label>
<input type="number" id="fFuelUsed" placeholder="0">
</div>
<div class="form-group">
<label>Следующее ТО</label>
<input type="date" id="fNextTo">
</div>
<div class="form-group">
<label>Статус</label>
<select id="fStatus">
<option value="в работе">в работе</option>
<option value="закрыт">закрыт</option>
<option value="просрочен">просрочен</option>
<option value="требуется ТО">требуется ТО</option>
</select>
</div>
<div class="form-group full">
<label>Комментарий</label>
<textarea id="fNote" rows="3" placeholder="Примечания"></textarea>
</div>
</div>
<div class="modal-actions">
<button class="btn btn-secondary" id="btnCancel">Отмена</button>
<button class="btn btn-primary" id="btnSave">Сохранить</button>
</div>
</div>
</div>
<div class="confirm-overlay" id="confirmOverlay">
<div class="confirm">
<h3>Удаление записи</h3>
<p>Вы уверены, что хотите удалить этот путевой лист? Это действие нельзя отменить.</p>
<div class="confirm-actions">
<button class="btn btn-secondary" id="btnConfirmCancel">Отмена</button>
<button class="btn btn-danger" id="btnConfirmDelete">Удалить</button>
</div>
</div>
</div>
<script>
const STORAGE_KEY = 'auto_fleet_data';
const LOG_KEY = 'auto_fleet_log';
const demoData = [
{ id: 1, number: 'ПЛ-2024-089', date: '2024-12-15', car: 'Toyota Camry', driver: 'Ерлан Ахметов', route: 'Алматы - Астана', startKm: 45230, endKm: 46180, mileage: 950, fuelGiven: 85, fuelUsed: 78, nextTo: '2025-01-10', status: 'в работе', note: 'Плановая поездка' },
{ id: 2, number: 'ПЛ-2024-090', date: '2024-12-18', car: 'Hyundai Elantra', driver: 'Айдос Каримов', route: 'Алматы - Шымкент', startKm: 32100, endKm: 32680, mileage: 580, fuelGiven: 52, fuelUsed: 48, nextTo: '2025-02-15', status: 'закрыт', note: '' },
{ id: 3, number: 'ПЛ-2024-091', date: '2024-12-20', car: 'Kia Sorento', driver: 'Гульмира Омарова', route: 'Астана - Караганда', startKm: 28450, endKm: 28650, mileage: 200, fuelGiven: 25, fuelUsed: 22, nextTo: '2024-12-28', status: 'требуется ТО', note: 'Замена масла' },
{ id: 4, number: 'ПЛ-2024-092', date: '2024-12-10', car: 'Toyota Camry', driver: 'Нурлан Сериков', route: 'Алматы - Талдыкорган', startKm: 45100, endKm: 45450, mileage: 350, fuelGiven: 35, fuelUsed: 32, nextTo: '2025-01-10', status: 'закрыт', note: '' },
{ id: 5, number: 'ПЛ-2024-093', date: '2024-12-05', car: 'Hyundai Elantra', driver: 'Руслан Баймуханов', route: 'Шымкент - Туркестан', startKm: 31800, endKm: 31950, mileage: 150, fuelGiven: 18, fuelUsed: 15, nextTo: '2025-02-15', status: 'просрочен', note: 'Задержка с возвратом' },
{ id: 6, number: 'ПЛ-2024-094', date: '2024-12-22', car: 'Kia Sorento', driver: 'Алия Нурпеисова', route: 'Астана - Кокшетау', startKm: 28700, endKm: 29050, mileage: 350, fuelGiven: 40, fuelUsed: 36, nextTo: '2025-01-05', status: 'в работе', note: 'Делегация' }
];
let data = loadData();
let currentFilter = 'все';
let currentId = null;
let deleteId = null;
function loadData() {
const stored = localStorage.getItem(STORAGE_KEY);
if (stored) return JSON.parse(stored);
localStorage.setItem(STORAGE_KEY, JSON.stringify(demoData));
return [...demoData];
}
function saveData() {
localStorage.setItem(STORAGE_KEY, JSON.stringify(data));
}
function loadLog() {
const stored = localStorage.getItem(LOG_KEY);
return stored ? JSON.parse(stored) : [];
}
function addLog(action) {
const log = loadLog();
log.unshift({ time: new Date().toISOString(), action });
localStorage.setItem(LOG_KEY, JSON.stringify(log.slice(0, 50)));
renderLog();
}
function formatDate(dateStr) {
if (!dateStr) return '';
const d = new Date(dateStr);
return d.toLocaleDateString('ru-RU', { day: '2-digit', month: '2-digit', year: 'numeric' });
}
function getStatusClass(status) {
const map = { 'в работе': 'status-in-work', 'закрыт': 'status-closed', 'просрочен': 'status-overdue', 'требуется ТО': 'status-maintenance' };
return map[status] || '';
}
function getStats() {
const stats = { total: data.length, inWork: 0, closed: 0, overdue: 0, maintenance: 0 };
data.forEach(d => {
if (d.status === 'в работе') stats.inWork++;
else if (d.status === 'закрыт') stats.closed++;
else if (d.status === 'просрочен') stats.overdue++;
else if (d.status === 'требуется ТО') stats.maintenance++;
});
return stats;
}
function renderStats() {
const s = getStats();
document.getElementById('stats').innerHTML = `
<div class="stat"><div class="stat-value">${s.total}</div><div class="stat-label">Всего</div></div>
<div class="stat in-work"><div class="stat-value">${s.inWork}</div><div class="stat-label">В работе</div></div>
<div class="stat closed"><div class="stat-value">${s.closed}</div><div class="stat-label">Закрыт</div></div>
<div class="stat overdue"><div class="stat-value">${s.overdue}</div><div class="stat-label">Просрочен</div></div>
<div class="stat maintenance"><div class="stat-value">${s.maintenance}</div><div class="stat-label">Требуется ТО</div></div>
`;
}
function renderChips() {
const chips = ['все', 'в работе', 'закрыт', 'просрочен', 'требуется ТО'];
document.getElementById('chips').innerHTML = chips.map(c =>
`<div class="chip ${c === currentFilter ? 'active' : ''}" data-filter="${c}">${c}</div>`
).join('');
document.querySelectorAll('.chip').forEach(el => {
el.addEventListener('click', () => {
currentFilter = el.dataset.filter;
renderChips();
renderTable();
});
});
}
function getFilteredData() {
const query = document.getElementById('search').value.toLowerCase();
return data.filter(d => {
const matchFilter = currentFilter === 'все' || d.status === currentFilter;
const matchSearch = !query ||
d.driver.toLowerCase().includes(query) ||
d.car.toLowerCase().includes(query) ||
d.route.toLowerCase().includes(query) ||
d.number.toLowerCase().includes(query);
return matchFilter && matchSearch;
});
}
function renderTable() {
const filtered = getFilteredData();
const tbody = document.getElementById('tableBody');
const empty = document.getElementById('empty');
if (filtered.length === 0) {
tbody.innerHTML = '';
empty.style.display = 'block';
return;
}
empty.style.display = 'none';
tbody.innerHTML = filtered.map(d => `
<tr data-id="${d.id}">
<td>${d.number}</td>
<td>${formatDate(d.date)}</td>
<td>${d.car}</td>
<td>${d.driver}</td>
<td>${d.route}</td>
<td>${d.mileage}</td>
<td><span class="status-badge ${getStatusClass(d.status)}">${d.status}</span></td>
</tr>
`).join('');
tbody.querySelectorAll('tr').forEach(tr => {
tr.addEventListener('click', (e) => {
if (e.target.closest('.status-badge')) return;
const id = parseInt(tr.dataset.id);
openCard(id);
});
});
}
function renderLog() {
const log = loadLog();
document.getElementById('logList').innerHTML = log.length ? log.map(l =>
`<div class="log-item"><span class="log-time">${new Date(l.time).toLocaleString('ru-RU')}</span>${l.action}</div>`
).join('') : '<div class="log-item">Нет записей</div>';
}
function openModal(id = null) {
currentId = id;
const overlay = document.getElementById('modalOverlay');
const title = document.getElementById('modalTitle');
if (id) {
const d = data.find(x => x.id === id);
title.textContent = 'Редактирование путевого листа';
document.getElementById('fNumber').value = d.number;
document.getElementById('fDate').value = d.date;
document.getElementById('fCar').value = d.car;
document.getElementById('fDriver').value = d.driver;
document.getElementById('fRoute').value = d.route;
document.getElementById('fStartKm').value = d.startKm;
document.getElementById('fEndKm').value = d.endKm;
document.getElementById('fFuelGiven').value = d.fuelGiven;
document.getElementById('fFuelUsed').value = d.fuelUsed;
document.getElementById('fNextTo').value = d.nextTo;
document.getElementById('fStatus').value = d.status;
document.getElementById('fNote').value = d.note || '';
} else {
title.textContent = 'Новый путевой лист';
document.getElementById('fNumber').value = '';
document.getElementById('fDate').value = new Date().toISOString().slice(0, 10);
document.getElementById('fCar').value = '';
document.getElementById('fDriver').value = '';
document.getElementById('fRoute').value = '';
document.getElementById('fStartKm').value = '';
document.getElementById('fEndKm').value = '';
document.getElementById('fFuelGiven').value = '';
document.getElementById('fFuelUsed').value = '';
document.getElementById('fNextTo').value = '';
document.getElementById('fStatus').value = 'в работе';
document.getElementById('fNote').value = '';
}
overlay.classList.add('open');
}
function closeModal() {
document.getElementById('modalOverlay').classList.remove('open');
currentId = null;
}
function saveForm() {
const number = document.getElementById('fNumber').value.trim();
const date = document.getElementById('fDate').value;
const car = document.getElementById('fCar').value.trim();
const driver = document.getElementById('fDriver').value.trim();
const route = document.getElementById('fRoute').value.trim();
const startKm = parseInt(document.getElementById('fStartKm').value) || 0;
const endKm = parseInt(document.getElementById('fEndKm').value) || 0;
const fuelGiven = parseInt(document.getElementById('fFuelGiven').value) || 0;
const fuelUsed = parseInt(document.getElementById('fFuelUsed').value) || 0;
const nextTo = document.getElementById('fNextTo').value;
const status = document.getElementById('fStatus').value;
const note = document.getElementById('fNote').value.trim();
if (!number || !date || !car || !driver) {
alert('Заполните обязательные поля: номер, дата, автомобиль, водитель');
return;
}
if (currentId) {
const idx = data.findIndex(x => x.id === currentId);
const old = data[idx];
data[idx] = { ...old, number, date, car, driver, route, startKm, endKm, mileage: endKm - startKm, fuelGiven, fuelUsed, nextTo, status, note };
addLog(`Изменён ПЛ ${number}: ${old.status} → ${status}, водитель: ${driver}`);
} else {
const newId = Math.max(...data.map(d => d.id), 0) + 1;
data.unshift({ id: newId, number, date, car, driver, route, startKm, endKm, mileage: endKm - startKm, fuelGiven, fuelUsed, nextTo, status, note });
addLog(`Добавлен ПЛ ${number}, водитель: ${driver}, статус: ${status}`);
}
saveData();
closeModal();
renderAll();
}
function openCard(id) {
const d = data.find(x => x.id === id);
const html = `
<div style="font-size:14px;line-height:1.8">
<p><strong>Номер:</strong> ${d.number}</p>
<p><strong>Дата:</strong> ${formatDate(d.date)}</p>
<p><strong>Автомобиль:</strong> ${d.car}</p>
<p><strong>Водитель:</strong> ${d.driver}</p>
<p><strong>Маршрут:</strong> ${d.route}</p>
<p><strong>Километраж:</strong> ${d.startKm} → ${d.endKm} км (пробег: ${d.mileage} км)</p>
<p><strong>Топливо:</strong> выдано ${d.fuelGiven} л, израсходовано ${d.fuelUsed} л</p>
<p><strong>Следующее ТО:</strong> ${formatDate(d.nextTo) || 'не назначено'}</p>
<p><strong>Статус:</strong> <span class="status-badge ${getStatusClass(d.status)}">${d.status}</span></p>
<p><strong>Комментарий:</strong> ${d.note || '—'}</p>
</div>
`;
const overlay = document.getElementById('modalOverlay');
document.getElementById('modalTitle').textContent = 'Карточка путевого листа';
document.querySelector('.modal .form-grid').innerHTML = html;
document.querySelector('.modal .form-grid').className = '';
document.querySelector('.modal-actions').style.display = 'none';
overlay.classList.add('open');
const editBtn = document.createElement('button');
editBtn.className = 'btn btn-primary';
editBtn.textContent = 'Редактировать';
editBtn.style.marginRight = '8px';
editBtn.onclick = () => { overlay.classList.remove('open'); openModal(id); };
const delBtn = document.createElement('button');
delBtn.className = 'btn btn-danger';
delBtn.textContent = 'Удалить';
delBtn.onclick = () => { overlay.classList.remove('open'); confirmDelete(id); };
const cancelBtn = document.createElement('button');
cancelBtn.className = 'btn btn-secondary';
cancelBtn.textContent = 'Закрыть';
cancelBtn.style.marginLeft = '8px';
cancelBtn.onclick = () => { overlay.classList.remove('open'); renderAll(); };
const actions = document.createElement('div');
actions.className = 'modal-actions';
actions.style.display = 'flex';
actions.appendChild(editBtn);
actions.appendChild(delBtn);
actions.appendChild(cancelBtn);
document.querySelector('.modal').insertBefore(actions, document.querySelector('.modal').lastElementChild);
}
function confirmDelete(id) {
deleteId = id;
document.getElementById('confirmOverlay').classList.add('open');
}
function executeDelete() {
const d = data.find(x => x.id === deleteId);
if (d) {
addLog(`Удалён ПЛ ${d.number}, водитель: ${d.driver}`);
data = data.filter(x => x.id !== deleteId);
saveData();
renderAll();
}
document.getElementById('confirmOverlay').classList.remove('open');
deleteId = null;
}
function exportCsv() {
const head = ['Номер ПЛ', 'Дата', 'Автомобиль', 'Водитель', 'Маршрут', 'Нач. км', 'Кон. км', 'Пробег', 'Выдано топлива', 'Расход', 'След. ТО', 'Статус', 'Комментарий'];
const body = data.map(d => [
d.number, formatDate(d.date), d.car, d.driver, d.route, d.startKm, d.endKm, d.mileage, d.fuelGiven, d.fuelUsed, formatDate(d.nextTo), d.status, d.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 = 'putevye_listy.csv';
a.click();
addLog('Выгрузка данных в CSV');
}
function renderAll() {
renderStats();
renderChips();
renderTable();
renderLog();
}
document.getElementById('btnAdd').onclick = () => openModal();
document.getElementById('btnCancel').onclick = closeModal;
document.getElementById('btnSave').onclick = saveForm;
document.getElementById('btnExport').onclick = exportCsv;
document.getElementById('btnConfirmDelete').onclick = executeDelete;
document.getElementById('btnConfirmCancel').onclick = () => {
document.getElementById('confirmOverlay').classList.remove('open');
deleteId = null;
};
document.getElementById('search').addEventListener('input', renderTable);
document.getElementById('modalOverlay').addEventListener('click', (e) => {
if (e.target === e.currentTarget) closeModal();
});
document.getElementById('confirmOverlay').addEventListener('click', (e) => {
if (e.target === e.currentTarget) {
document.getElementById('confirmOverlay').classList.remove('open');
deleteId = null;
}
});
renderAll();
</script>
</body>
</html>