411 lines
14 KiB
JavaScript
411 lines
14 KiB
JavaScript
const STORAGE_KEY = 'uchet_komandirovok_data';
|
||
const CURRENT_USER = 'Ответственный за учёт';
|
||
|
||
const DEMO_DATA = {
|
||
items: [
|
||
{
|
||
id: 1,
|
||
employee: 'Айдос Нурланов',
|
||
city: 'Астана',
|
||
dateStart: '2026-08-12',
|
||
dateEnd: '2026-08-15',
|
||
purpose: 'Участие в международной конференции по цифровизации',
|
||
amount: 185000,
|
||
status: 'согласовано'
|
||
},
|
||
{
|
||
id: 2,
|
||
employee: 'Гульназ Сейдахметова',
|
||
city: 'Алматы',
|
||
dateStart: '2026-09-03',
|
||
dateEnd: '2026-09-05',
|
||
purpose: 'Переговоры с партнёрами по проекту',
|
||
amount: 95000,
|
||
status: 'на согласовании'
|
||
},
|
||
{
|
||
id: 3,
|
||
employee: 'Ержан Тлеубергенов',
|
||
city: 'Шымкент',
|
||
dateStart: '2026-09-18',
|
||
dateEnd: '2026-09-20',
|
||
purpose: 'Обучение сотрудников филиала',
|
||
amount: 72000,
|
||
status: 'на согласовании'
|
||
},
|
||
{
|
||
id: 4,
|
||
employee: 'Алия Касымова',
|
||
city: 'Караганда',
|
||
dateStart: '2026-10-05',
|
||
dateEnd: '2026-10-07',
|
||
purpose: 'Аудит регионального офиса',
|
||
amount: 68000,
|
||
status: 'согласовано'
|
||
},
|
||
{
|
||
id: 5,
|
||
employee: 'Айдос Нурланов',
|
||
city: 'Актобе',
|
||
dateStart: '2026-10-12',
|
||
dateEnd: '2026-10-14',
|
||
purpose: 'Презентация нового продукта',
|
||
amount: 89000,
|
||
status: 'отклонено'
|
||
},
|
||
{
|
||
id: 6,
|
||
employee: 'Гульназ Сейдахметова',
|
||
city: 'Астана',
|
||
dateStart: '2026-11-02',
|
||
dateEnd: '2026-11-06',
|
||
purpose: 'Участие в выставке EXPO',
|
||
amount: 145000,
|
||
status: 'согласовано'
|
||
},
|
||
{
|
||
id: 7,
|
||
employee: 'Ержан Тлеубергенов',
|
||
city: 'Алматы',
|
||
dateStart: '2026-11-15',
|
||
dateEnd: '2026-11-17',
|
||
purpose: 'Встреча с инвесторами',
|
||
amount: 112000,
|
||
status: 'на согласовании'
|
||
}
|
||
],
|
||
log: [
|
||
{ when: '2026-08-01 10:23', what: 'Создана запись: Айдос Нурланов — Астана', who: 'Ответственный за учёт' },
|
||
{ when: '2026-08-01 14:15', what: 'Статус изменён на «согласовано»: Айдос Нурланов', who: 'Ответственный за учёт' },
|
||
{ when: '2026-08-02 09:45', what: 'Создана запись: Гульназ Сейдахметова — Алматы', who: 'Ответственный за учёт' },
|
||
{ when: '2026-08-02 11:30', what: 'Создана запись: Ержан Тлеубергенов — Шымкент', who: 'Ответственный за учёт' },
|
||
{ when: '2026-08-03 16:20', what: 'Создана запись: Алия Касымова — Караганда', who: 'Ответственный за учёт' },
|
||
{ when: '2026-08-04 10:00', what: 'Статус изменён на «согласовано»: Алия Касымова', who: 'Ответственный за учёт' },
|
||
{ when: '2026-08-05 13:45', what: 'Создана запись: Айдос Нурланов — Актобе', who: 'Ответственный за учёт' },
|
||
{ when: '2026-08-05 15:30', what: 'Статус изменён на «отклонено»: Айдос Нурланов', who: 'Ответственный за учёт' },
|
||
{ when: '2026-08-06 09:15', what: 'Создана запись: Гульназ Сейдахметова — Астана', who: 'Ответственный за учёт' },
|
||
{ when: '2026-08-07 11:00', what: 'Создана запись: Ержан Тлеубергенов — Алматы', who: 'Ответственный за учёт' }
|
||
]
|
||
};
|
||
|
||
function loadData() {
|
||
try {
|
||
const stored = localStorage.getItem(STORAGE_KEY);
|
||
if (stored) {
|
||
return JSON.parse(stored);
|
||
}
|
||
} catch (e) {
|
||
console.error('Ошибка загрузки данных:', e);
|
||
}
|
||
return JSON.parse(JSON.stringify(DEMO_DATA));
|
||
}
|
||
|
||
function saveData(data) {
|
||
localStorage.setItem(STORAGE_KEY, JSON.stringify(data));
|
||
}
|
||
|
||
function formatDate(dateStr) {
|
||
const [year, month, day] = dateStr.split('-');
|
||
return `${day}.${month}.${year}`;
|
||
}
|
||
|
||
function formatDateTime() {
|
||
const now = new Date();
|
||
const year = now.getFullYear();
|
||
const month = String(now.getMonth() + 1).padStart(2, '0');
|
||
const day = String(now.getDate()).padStart(2, '0');
|
||
const hours = String(now.getHours()).padStart(2, '0');
|
||
const minutes = String(now.getMinutes()).padStart(2, '0');
|
||
return `${year}-${month}-${day} ${hours}:${minutes}`;
|
||
}
|
||
|
||
function getStatusClass(status) {
|
||
if (status === 'согласовано') return 'status-approved';
|
||
if (status === 'отклонено') return 'status-rejected';
|
||
return 'status-pending';
|
||
}
|
||
|
||
function getStatusLabel(status) {
|
||
if (status === 'согласовано') return '✓ Согласовано';
|
||
if (status === 'отклонено') return '✗ Отклонено';
|
||
return '⏳ На согласовании';
|
||
}
|
||
|
||
let data = loadData();
|
||
let currentFilter = '';
|
||
let searchQuery = '';
|
||
|
||
function renderStats() {
|
||
const total = data.items.length;
|
||
const pending = data.items.filter(i => i.status === 'на согласовании').length;
|
||
const approved = data.items.filter(i => i.status === 'согласовано').length;
|
||
const rejected = data.items.filter(i => i.status === 'отклонено').length;
|
||
|
||
document.getElementById('stat-total').textContent = total;
|
||
document.getElementById('stat-pending').textContent = pending;
|
||
document.getElementById('stat-approved').textContent = approved;
|
||
document.getElementById('stat-rejected').textContent = rejected;
|
||
}
|
||
|
||
function renderTable() {
|
||
const tbody = document.getElementById('table-body');
|
||
|
||
let filtered = data.items;
|
||
|
||
if (currentFilter) {
|
||
filtered = filtered.filter(i => i.status === currentFilter);
|
||
}
|
||
|
||
if (searchQuery) {
|
||
const q = searchQuery.toLowerCase();
|
||
filtered = filtered.filter(i =>
|
||
i.employee.toLowerCase().includes(q) ||
|
||
i.city.toLowerCase().includes(q) ||
|
||
i.purpose.toLowerCase().includes(q)
|
||
);
|
||
}
|
||
|
||
if (filtered.length === 0) {
|
||
tbody.innerHTML = `
|
||
<tr>
|
||
<td colspan="7" class="empty-state">
|
||
<div>
|
||
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5">
|
||
<path d="M9 12h6m-6 4h6m2 5H7a2 2 0 01-2-2V5a2 2 0 012-2h5.586a1 1 0 01.707.293l5.414 5.414a1 1 0 01.293.707V19a2 2 0 01-2 2z"/>
|
||
</svg>
|
||
<p>Нет записей для отображения</p>
|
||
</div>
|
||
</td>
|
||
</tr>
|
||
`;
|
||
return;
|
||
}
|
||
|
||
tbody.innerHTML = filtered.map(item => `
|
||
<tr data-id="${item.id}">
|
||
<td><strong>${item.employee}</strong></td>
|
||
<td>${item.city}</td>
|
||
<td>${formatDate(item.dateStart)} – ${formatDate(item.dateEnd)}</td>
|
||
<td>${item.purpose}</td>
|
||
<td>${item.amount.toLocaleString('ru-RU')}</td>
|
||
<td><span class="status-badge ${getStatusClass(item.status)}">${getStatusLabel(item.status)}</span></td>
|
||
<td>
|
||
<div class="action-buttons">
|
||
<button class="btn-sm btn-edit" onclick="editItem(${item.id})">✏️</button>
|
||
<button class="btn-sm btn-delete" onclick="deleteItem(${item.id})">🗑️</button>
|
||
</div>
|
||
</td>
|
||
</tr>
|
||
`).join('');
|
||
}
|
||
|
||
function renderLog() {
|
||
const tbody = document.getElementById('log-body');
|
||
if (data.log.length === 0) {
|
||
tbody.innerHTML = '<tr><td colspan="3" style="text-align:center;color:#9ca3af;">Журнал пуст</td></tr>';
|
||
return;
|
||
}
|
||
|
||
const sortedLog = [...data.log].reverse();
|
||
tbody.innerHTML = sortedLog.map(entry => `
|
||
<tr>
|
||
<td>${entry.when}</td>
|
||
<td>${entry.what}</td>
|
||
<td>${entry.who}</td>
|
||
</tr>
|
||
`).join('');
|
||
}
|
||
|
||
function openModal(modalId) {
|
||
document.getElementById(modalId).classList.add('active');
|
||
}
|
||
|
||
function closeModal(modalId) {
|
||
document.getElementById(modalId).classList.remove('active');
|
||
}
|
||
|
||
function openForm(id = null) {
|
||
const modal = document.getElementById('modal-form');
|
||
const title = document.getElementById('modal-title');
|
||
const form = document.getElementById('form');
|
||
|
||
form.reset();
|
||
document.getElementById('form-id').value = '';
|
||
|
||
if (id) {
|
||
const item = data.items.find(i => i.id === id);
|
||
if (item) {
|
||
title.textContent = 'Редактировать командировку';
|
||
document.getElementById('form-id').value = item.id;
|
||
document.getElementById('form-employee').value = item.employee;
|
||
document.getElementById('form-city').value = item.city;
|
||
document.getElementById('form-date-start').value = item.dateStart;
|
||
document.getElementById('form-date-end').value = item.dateEnd;
|
||
document.getElementById('form-purpose').value = item.purpose;
|
||
document.getElementById('form-amount').value = item.amount;
|
||
document.getElementById('form-status').value = item.status;
|
||
}
|
||
} else {
|
||
title.textContent = 'Добавить командировку';
|
||
}
|
||
|
||
openModal('modal-form');
|
||
}
|
||
|
||
function editItem(id) {
|
||
openForm(id);
|
||
}
|
||
|
||
function deleteItem(id) {
|
||
const item = data.items.find(i => i.id === id);
|
||
if (!item) return;
|
||
|
||
if (confirm(`Вы уверены, что хотите удалить запись?\n\nСотрудник: ${item.employee}\nГород: ${item.city}\nДаты: ${formatDate(item.dateStart)} – ${formatDate(item.dateEnd)}`)) {
|
||
const logEntry = {
|
||
when: formatDateTime(),
|
||
what: `Удалена запись: ${item.employee} — ${item.city}`,
|
||
who: CURRENT_USER
|
||
};
|
||
|
||
data.items = data.items.filter(i => i.id !== id);
|
||
data.log.push(logEntry);
|
||
saveData(data);
|
||
|
||
renderStats();
|
||
renderTable();
|
||
renderLog();
|
||
}
|
||
}
|
||
|
||
function addItem(item) {
|
||
const logEntry = {
|
||
when: formatDateTime(),
|
||
what: `Создана запись: ${item.employee} — ${item.city}`,
|
||
who: CURRENT_USER
|
||
};
|
||
|
||
data.items.push(item);
|
||
data.log.push(logEntry);
|
||
saveData(data);
|
||
|
||
renderStats();
|
||
renderTable();
|
||
renderLog();
|
||
}
|
||
|
||
function updateItem(id, updatedItem) {
|
||
const oldItem = data.items.find(i => i.id === id);
|
||
if (!oldItem) return;
|
||
|
||
const changes = [];
|
||
if (oldItem.employee !== updatedItem.employee) changes.push(`сотрудник: ${oldItem.employee} → ${updatedItem.employee}`);
|
||
if (oldItem.city !== updatedItem.city) changes.push(`город: ${oldItem.city} → ${updatedItem.city}`);
|
||
if (oldItem.status !== updatedItem.status) changes.push(`статус: ${oldItem.status} → ${updatedItem.status}`);
|
||
|
||
const logEntry = {
|
||
when: formatDateTime(),
|
||
what: `Изменена запись: ${oldItem.employee} — ${item.city}` + (changes.length > 0 ? ` (${changes.join(', ')})` : ''),
|
||
who: CURRENT_USER
|
||
};
|
||
|
||
const index = data.items.findIndex(i => i.id === id);
|
||
data.items[index] = { ...updatedItem, id };
|
||
data.log.push(logEntry);
|
||
saveData(data);
|
||
|
||
renderStats();
|
||
renderTable();
|
||
renderLog();
|
||
}
|
||
|
||
function exportCsv() {
|
||
const headers = ['Сотрудник', 'Город', 'Дата начала', 'Дата окончания', 'Цель', 'Сумма (тенге)', 'Статус'];
|
||
const rows = data.items.map(item => [
|
||
item.employee,
|
||
item.city,
|
||
formatDate(item.dateStart),
|
||
formatDate(item.dateEnd),
|
||
item.purpose,
|
||
item.amount.toString(),
|
||
item.status
|
||
]);
|
||
|
||
const csvContent = '\uFEFF' + [
|
||
headers.join(';'),
|
||
...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 = 'komandirovki_' + new Date().toISOString().slice(0, 10) + '.csv';
|
||
link.click();
|
||
URL.revokeObjectURL(link.href);
|
||
}
|
||
|
||
document.addEventListener('DOMContentLoaded', () => {
|
||
renderStats();
|
||
renderTable();
|
||
renderLog();
|
||
|
||
document.getElementById('btn-add').addEventListener('click', () => openForm());
|
||
|
||
document.getElementById('close-form').addEventListener('click', () => closeModal('modal-form'));
|
||
document.getElementById('cancel-form').addEventListener('click', () => closeModal('modal-form'));
|
||
|
||
document.getElementById('modal-form').addEventListener('click', (e) => {
|
||
if (e.target.id === 'modal-form') closeModal('modal-form');
|
||
});
|
||
|
||
document.getElementById('btn-log').addEventListener('click', () => {
|
||
renderLog();
|
||
openModal('modal-log');
|
||
});
|
||
|
||
document.getElementById('close-log').addEventListener('click', () => closeModal('modal-log'));
|
||
|
||
document.getElementById('modal-log').addEventListener('click', (e) => {
|
||
if (e.target.id === 'modal-log') closeModal('modal-log');
|
||
});
|
||
|
||
document.getElementById('btn-export').addEventListener('click', exportCsv);
|
||
|
||
document.getElementById('search').addEventListener('input', (e) => {
|
||
searchQuery = e.target.value;
|
||
renderTable();
|
||
});
|
||
|
||
document.querySelectorAll('.chip').forEach(chip => {
|
||
chip.addEventListener('click', () => {
|
||
document.querySelectorAll('.chip').forEach(c => c.classList.remove('active'));
|
||
chip.classList.add('active');
|
||
currentFilter = chip.dataset.status;
|
||
renderTable();
|
||
});
|
||
});
|
||
|
||
document.getElementById('form').addEventListener('submit', (e) => {
|
||
e.preventDefault();
|
||
|
||
const id = document.getElementById('form-id').value;
|
||
const item = {
|
||
employee: document.getElementById('form-employee').value,
|
||
city: document.getElementById('form-city').value,
|
||
dateStart: document.getElementById('form-date-start').value,
|
||
dateEnd: document.getElementById('form-date-end').value,
|
||
purpose: document.getElementById('form-purpose').value,
|
||
amount: parseInt(document.getElementById('form-amount').value, 10),
|
||
status: document.getElementById('form-status').value
|
||
};
|
||
|
||
if (id) {
|
||
updateItem(parseInt(id, 10), item);
|
||
} else {
|
||
item.id = Date.now();
|
||
addItem(item);
|
||
}
|
||
|
||
closeModal('modal-form');
|
||
});
|
||
});
|