zhurnal-komandirovok-sotrudn/index.html

433 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="light">
<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>
.dashboard-stats { display: grid; grid-template-columns: repeat(auto-fit, minmax(200px, 1fr)); gap: var(--kt-ai-space-6); margin-bottom: var(--kt-ai-space-8); }
.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-5); }
.stat-value { font-size: 32px; font-weight: var(--kt-ai-weight-bold); color: var(--kt-ai-fg); margin-bottom: var(--kt-ai-space-1); }
.stat-label { color: var(--kt-ai-fg-muted); font-size: var(--kt-ai-text-sm); }
.controls-bar { display: flex; gap: var(--kt-ai-space-4); align-items: center; flex-wrap: wrap; margin-bottom: var(--kt-ai-space-6); }
.search-input { flex: 1; min-width: 250px; }
.filters { display: flex; gap: var(--kt-ai-space-2); flex-wrap: wrap; }
.table-container { background: var(--kt-ai-card-bg); border: 1px solid var(--kt-ai-card-border); border-radius: var(--kt-ai-radius-xl); overflow: hidden; }
.data-table { width: 100%; border-collapse: collapse; }
.data-table th, .data-table td { padding: var(--kt-ai-space-4); text-align: left; border-bottom: 1px solid var(--kt-ai-divider); }
.data-table th { background: var(--kt-ai-bg-soft); font-weight: var(--kt-ai-weight-semibold); color: var(--kt-ai-fg-muted); font-size: var(--kt-ai-text-sm); }
.data-table tr:hover { background: var(--kt-ai-bg-soft); cursor: pointer; }
.data-table tr:last-child td { border-bottom: none; }
.status-chip { display: inline-block; padding: 2px 10px; border-radius: var(--kt-ai-radius-full); font-size: var(--kt-ai-text-xs); font-weight: var(--kt-ai-weight-medium); }
.status-na-soglasovanii { background: #fef3c7; color: #92400e; }
.status-soglasovano { background: #d1fae5; color: #065f46; }
.status-otkazano { background: #fee2e2; color: #991b1b; }
.status-v-poezdke { background: #dbeafe; color: #1e40af; }
.status-zaversheno { background: #e5e7eb; color: #374151; }
.modal-overlay { position: fixed; inset: 0; background: rgba(0,0,0,0.5); display: none; align-items: center; justify-content: center; z-index: 1000; }
.modal-overlay.active { display: flex; }
.modal { background: var(--kt-ai-bg); border-radius: var(--kt-ai-radius-2xl); padding: var(--kt-ai-space-8); width: 100%; max-width: 500px; max-height: 90vh; overflow-y: auto; }
.modal-header { display: flex; justify-content: space-between; align-items: center; margin-bottom: var(--kt-ai-space-6); }
.modal-title { font-size: var(--kt-ai-text-xl); font-weight: var(--kt-ai-weight-semibold); }
.modal-close { background: none; border: none; font-size: 24px; cursor: pointer; color: var(--kt-ai-fg-muted); }
.form-group { margin-bottom: var(--kt-ai-space-5); }
.form-label { display: block; margin-bottom: var(--kt-ai-space-2); font-weight: var(--kt-ai-weight-medium); color: var(--kt-ai-fg); }
.form-input, .form-select { width: 100%; padding: var(--kt-ai-space-3); border: 1px solid var(--kt-ai-border); border-radius: var(--kt-ai-radius-lg); background: var(--kt-ai-bg); color: var(--kt-ai-fg); font-size: var(--kt-ai-text-base); }
.form-input:focus, .form-select:focus { outline: 2px solid var(--kt-ai-primary); border-color: transparent; }
.modal-actions { display: flex; gap: var(--kt-ai-space-3); justify-content: flex-end; margin-top: var(--kt-ai-space-7); }
.log-container { margin-top: var(--kt-ai-space-8); padding: var(--kt-ai-space-5); background: var(--kt-ai-bg-soft); border-radius: var(--kt-ai-radius-xl); }
.log-title { font-weight: var(--kt-ai-weight-semibold); margin-bottom: var(--kt-ai-space-3); }
.log-list { list-style: none; padding: 0; margin: 0; max-height: 200px; overflow-y: auto; }
.log-item { padding: var(--kt-ai-space-2) 0; border-bottom: 1px solid var(--kt-ai-divider); font-size: var(--kt-ai-text-sm); color: var(--kt-ai-fg-muted); }
.page-header { display: flex; justify-content: space-between; align-items: center; margin-bottom: var(--kt-ai-space-8); flex-wrap: wrap; gap: var(--kt-ai-space-4); }
.page-title { font-size: var(--kt-ai-text-2xl); font-weight: var(--kt-ai-weight-bold); }
.empty-state { text-align: center; padding: var(--kt-ai-space-12); color: var(--kt-ai-fg-muted); }
.btn-icon { display: inline-flex; align-items: center; gap: 6px; }
</style>
</head>
<body>
<header class="kt-ai-pagehead">
<div class="kt-ai-wrap kt-ai-pagehead-in">
<strong>Журнал командировок</strong>
<span class="kt-ai-spacer"></span>
<button class="kt-ai-btn" data-variant="ghost" onclick="exportToExcel()">
<svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4"/><polyline points="7 10 12 15 17 10"/><line x1="12" y1="15" x2="12" y2="3"/></svg>
Выгрузить в Excel
</button>
</div>
</header>
<main class="kt-ai-wrap" style="padding-top: var(--kt-ai-space-8); padding-bottom: var(--kt-ai-space-8);">
<div class="page-header">
<h1 class="page-title">Дашборд командировок</h1>
<button class="kt-ai-btn" data-variant="primary" data-size="lg" onclick="openModal()">
<svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><line x1="12" y1="5" x2="12" y2="19"/><line x1="5" y1="12" x2="19" y2="12"/></svg>
Добавить командировку
</button>
</div>
<div class="dashboard-stats">
<div class="stat-card">
<div class="stat-value" id="stat-total">0</div>
<div class="stat-label">Всего командировок</div>
</div>
<div class="stat-card">
<div class="stat-value" id="stat-na-soglasovanii">0</div>
<div class="stat-label">На согласовании</div>
</div>
<div class="stat-card">
<div class="stat-value" id="stat-soglasovano">0</div>
<div class="stat-label">Согласовано</div>
</div>
<div class="stat-card">
<div class="stat-value" id="stat-v-poezdke">0</div>
<div class="stat-label">В поездке</div>
</div>
</div>
<div class="controls-bar">
<input type="text" class="kt-ai-input search-input" id="search-input" placeholder="Поиск по ФИО или городу...">
<div class="filters" id="status-filters">
<button class="kt-ai-btn" data-variant="primary" data-status="all">Все</button>
<button class="kt-ai-btn" data-variant="ghost" data-status="На согласовании">На согласовании</button>
<button class="kt-ai-btn" data-variant="ghost" data-status="Согласовано">Согласовано</button>
<button class="kt-ai-btn" data-variant="ghost" data-status="Отказано">Отказано</button>
<button class="kt-ai-btn" data-variant="ghost" data-status="В поездке">В поездке</button>
</div>
</div>
<div class="table-container">
<table class="data-table">
<thead>
<tr>
<th>Сотрудник</th>
<th>Куда</th>
<th>Даты</th>
<th>Бюджет</th>
<th>Статус</th>
<th>Действия</th>
</tr>
</thead>
<tbody id="table-body">
</tbody>
</table>
<div id="empty-state" class="empty-state" style="display: none;">
<p>Нет записей</p>
</div>
</div>
<div class="log-container">
<div class="log-title">Журнал изменений</div>
<ul class="log-list" id="log-list"></ul>
</div>
</main>
<div class="modal-overlay" id="modal-overlay">
<div class="modal">
<div class="modal-header">
<h2 class="modal-title" id="modal-title">Добавить командировку</h2>
<button class="modal-close" onclick="closeModal()">&times;</button>
</div>
<form id="trip-form" onsubmit="saveTrip(event)">
<input type="hidden" id="trip-id">
<div class="form-group">
<label class="form-label" for="employee-name">ФИО сотрудника</label>
<input type="text" class="form-input" id="employee-name" required placeholder="Иванов А.А.">
</div>
<div class="form-group">
<label class="form-label" for="destination">Город назначения</label>
<input type="text" class="form-input" id="destination" required placeholder="Астана">
</div>
<div class="form-group">
<label class="form-label" for="date-start">Дата начала</label>
<input type="date" class="form-input" id="date-start" required>
</div>
<div class="form-group">
<label class="form-label" for="date-end">Дата окончания</label>
<input type="date" class="form-input" id="date-end" required>
</div>
<div class="form-group">
<label class="form-label" for="budget">Бюджет (тенге)</label>
<input type="number" class="form-input" id="budget" required placeholder="150000" min="0">
</div>
<div class="form-group">
<label class="form-label" for="status">Статус</label>
<select class="form-select" id="status" required>
<option value="На согласовании">На согласовании</option>
<option value="Согласовано">Согласовано</option>
<option value="Отказано">Отказано</option>
<option value="В поездке">В поездке</option>
<option value="Завершено">Завершено</option>
</select>
</div>
<div class="modal-actions">
<button type="button" class="kt-ai-btn" data-variant="ghost" onclick="closeModal()">Отмена</button>
<button type="submit" class="kt-ai-btn" data-variant="primary">Сохранить</button>
</div>
</form>
</div>
</div>
<script>
const STORAGE_KEY = 'business-trips-journal';
const LOG_KEY = 'business-trips-log';
const DEMO_DATA = [
{ id: 1, employee: 'Иванов А.', destination: 'Астана', dateStart: '2023-10-10', dateEnd: '2023-10-15', budget: 150000, status: 'Согласовано' },
{ id: 2, employee: 'Петров Б.', destination: 'Алматы', dateStart: '2023-10-12', dateEnd: '2023-10-18', budget: 80000, status: 'На согласовании' },
{ id: 3, employee: 'Смирнов В.', destination: 'Москва', dateStart: '2023-11-01', dateEnd: '2023-11-10', budget: 450000, status: 'В поездке' },
{ id: 4, employee: 'Ахметов К.', destination: 'Астана', dateStart: '2023-11-05', dateEnd: '2023-11-12', budget: 120000, status: 'Отказано' },
{ id: 5, employee: 'Ли Д.', destination: 'Атырау', dateStart: '2023-11-20', dateEnd: '2023-11-25', budget: 90000, status: 'На согласовании' }
];
let trips = [];
let logs = [];
let currentFilter = 'all';
let searchQuery = '';
function loadData() {
const stored = localStorage.getItem(STORAGE_KEY);
if (stored) {
trips = JSON.parse(stored);
} else {
trips = [...DEMO_DATA];
saveData();
}
const storedLogs = localStorage.getItem(LOG_KEY);
logs = storedLogs ? JSON.parse(storedLogs) : [];
}
function saveData() {
localStorage.setItem(STORAGE_KEY, JSON.stringify(trips));
}
function saveLogs() {
localStorage.setItem(LOG_KEY, JSON.stringify(logs));
}
function addLog(action, details) {
const now = new Date();
logs.unshift({
timestamp: now.toLocaleString('ru-RU'),
action,
details
});
if (logs.length > 50) logs.pop();
saveLogs();
renderLog();
}
function formatDate(dateStr) {
const [year, month, day] = dateStr.split('-');
return `${day}.${month}.${year}`;
}
function formatBudget(amount) {
return new Intl.NumberFormat('ru-RU').format(amount) + ' ₸';
}
function getStatusClass(status) {
const map = {
'На согласовании': 'status-na-soglasovanii',
'Согласовано': 'status-soglasovano',
'Отказано': 'status-otkazano',
'В поездке': 'status-v-poezdke',
'Завершено': 'status-zaversheno'
};
return map[status] || '';
}
function updateStats() {
const total = trips.length;
const naSoglasovanii = trips.filter(t => t.status === 'На согласовании').length;
const soglasovano = trips.filter(t => t.status === 'Согласовано').length;
const vPoezdke = trips.filter(t => t.status === 'В поездке').length;
document.getElementById('stat-total').textContent = total;
document.getElementById('stat-na-soglasovanii').textContent = naSoglasovanii;
document.getElementById('stat-soglasovano').textContent = soglasovano;
document.getElementById('stat-v-poezdke').textContent = vPoezdke;
}
function getFilteredTrips() {
return trips.filter(trip => {
const matchesStatus = currentFilter === 'all' || trip.status === currentFilter;
const matchesSearch = searchQuery === '' ||
trip.employee.toLowerCase().includes(searchQuery.toLowerCase()) ||
trip.destination.toLowerCase().includes(searchQuery.toLowerCase());
return matchesStatus && matchesSearch;
});
}
function renderTable() {
const tbody = document.getElementById('table-body');
const emptyState = document.getElementById('empty-state');
const filtered = getFilteredTrips();
if (filtered.length === 0) {
tbody.innerHTML = '';
emptyState.style.display = 'block';
return;
}
emptyState.style.display = 'none';
tbody.innerHTML = filtered.map(trip => `
<tr onclick="editTrip(${trip.id})">
<td>${trip.employee}</td>
<td>${trip.destination}</td>
<td>${formatDate(trip.dateStart)} — ${formatDate(trip.dateEnd)}</td>
<td>${formatBudget(trip.budget)}</td>
<td><span class="status-chip ${getStatusClass(trip.status)}">${trip.status}</span></td>
<td>
<button class="kt-ai-btn" data-variant="ghost" data-size="sm" onclick="event.stopPropagation(); deleteTrip(${trip.id})">
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><polyline points="3 6 5 6 21 6"/><path d="M19 6v14a2 2 0 0 1-2 2H7a2 2 0 0 1-2-2V6m3 0V4a2 2 0 0 1 2-2h4a2 2 0 0 1 2 2v2"/></svg>
</button>
</td>
</tr>
`).join('');
}
function renderLog() {
const logList = document.getElementById('log-list');
logList.innerHTML = logs.map(log =>
`<li class="log-item"><strong>${log.timestamp}</strong> — ${log.action}: ${log.details}</li>`
).join('');
}
function openModal(tripId = null) {
const modal = document.getElementById('modal-overlay');
const title = document.getElementById('modal-title');
const form = document.getElementById('trip-form');
form.reset();
if (tripId) {
const trip = trips.find(t => t.id === tripId);
if (trip) {
title.textContent = 'Редактировать командировку';
document.getElementById('trip-id').value = trip.id;
document.getElementById('employee-name').value = trip.employee;
document.getElementById('destination').value = trip.destination;
document.getElementById('date-start').value = trip.dateStart;
document.getElementById('date-end').value = trip.dateEnd;
document.getElementById('budget').value = trip.budget;
document.getElementById('status').value = trip.status;
}
} else {
title.textContent = 'Добавить командировку';
document.getElementById('trip-id').value = '';
}
modal.classList.add('active');
}
function closeModal() {
document.getElementById('modal-overlay').classList.remove('active');
}
function saveTrip(event) {
event.preventDefault();
const id = document.getElementById('trip-id').value;
const tripData = {
employee: document.getElementById('employee-name').value,
destination: document.getElementById('destination').value,
dateStart: document.getElementById('date-start').value,
dateEnd: document.getElementById('date-end').value,
budget: parseInt(document.getElementById('budget').value),
status: document.getElementById('status').value
};
if (id) {
const index = trips.findIndex(t => t.id == id);
if (index !== -1) {
const oldStatus = trips[index].status;
trips[index] = { ...tripData, id: parseInt(id) };
addLog('Изменение', `Статус "${tripData.employee}" изменён с "${oldStatus}" на "${tripData.status}"`);
}
} else {
const newId = Date.now();
trips.push({ ...tripData, id: newId });
addLog('Добавление', `Новая командировка: ${tripData.employee} → ${tripData.destination}`);
}
saveData();
updateStats();
renderTable();
closeModal();
}
function editTrip(id) {
openModal(id);
}
function deleteTrip(id) {
if (!confirm('Удалить запись?')) return;
const trip = trips.find(t => t.id === id);
if (trip) {
addLog('Удаление', `${trip.employee} → ${trip.destination} (${trip.status})`);
trips = trips.filter(t => t.id !== id);
saveData();
updateStats();
renderTable();
}
}
function exportToExcel() {
const filtered = getFilteredTrips();
const headers = ['Сотрудник', 'Куда', 'Дата начала', 'Дата окончания', 'Бюджет', 'Статус'];
const rows = filtered.map(t => [
t.employee,
t.destination,
formatDate(t.dateStart),
formatDate(t.dateEnd),
t.budget.toString().replace('.', ','),
t.status
]);
const csvContent = '\uFEFF' + [headers, ...rows].map(row =>
row.map(cell => '"' + String(cell).replace(/"/g, '""') + '"').join(';')
).join('\n');
const blob = new Blob([csvContent], { type: 'text/csv;charset=utf-8;' });
const link = document.createElement('a');
link.href = URL.createObjectURL(blob);
link.download = 'journal.csv';
link.click();
addLog('Выгрузка', `Экспорт ${filtered.length} записей в CSV`);
}
document.getElementById('search-input').addEventListener('input', (e) => {
searchQuery = e.target.value;
renderTable();
});
document.getElementById('status-filters').addEventListener('click', (e) => {
if (e.target.classList.contains('kt-ai-btn')) {
document.querySelectorAll('#status-filters .kt-ai-btn').forEach(btn => {
btn.setAttribute('data-variant', 'ghost');
});
e.target.setAttribute('data-variant', 'primary');
currentFilter = e.target.getAttribute('data-status');
renderTable();
}
});
loadData();
updateStats();
renderTable();
renderLog();
</script>
</body>
</html>