uchet-komandirovok/script.js

411 lines
14 KiB
JavaScript
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.

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');
});
});