433 lines
13 KiB
JavaScript
433 lines
13 KiB
JavaScript
// Демо-данные: 8 обращений по требованиям брифа
|
||
const DEMO_DATA = [
|
||
{
|
||
id: 1,
|
||
date: '2026-08-20',
|
||
name: 'Айгүл Сәтбаева',
|
||
contact: '+7 (777) 123-45-67',
|
||
problem: 'Проблема с интернетом',
|
||
responsible: 'Ерлан Нурбаев',
|
||
deadline: '2026-08-28',
|
||
status: 'В работе'
|
||
},
|
||
{
|
||
id: 2,
|
||
date: '2026-08-22',
|
||
name: 'Данияр Төлегенов',
|
||
contact: '+7 (701) 234-56-78',
|
||
problem: 'Нет доступа к личному кабинету',
|
||
responsible: 'Гүлнұр Иманова',
|
||
deadline: '2026-08-27',
|
||
status: 'Новые'
|
||
},
|
||
{
|
||
id: 3,
|
||
date: '2026-08-18',
|
||
name: 'Нұрсұлтан Қасымов',
|
||
contact: '+7 (778) 345-67-89',
|
||
problem: 'Не проходит оплата',
|
||
responsible: 'Марат Серік',
|
||
deadline: '2026-08-25',
|
||
status: 'Выполнены'
|
||
},
|
||
{
|
||
id: 4,
|
||
date: '2026-08-19',
|
||
name: 'Асель Жумабаева',
|
||
contact: '+7 (702) 456-78-90',
|
||
problem: 'Низкая скорость соединения',
|
||
responsible: 'Айдана Оразалы',
|
||
deadline: '2026-08-26',
|
||
status: 'В работе'
|
||
},
|
||
{
|
||
id: 5,
|
||
date: '2026-08-21',
|
||
name: 'Бауыржан Омаров',
|
||
contact: '+7 (775) 567-89-01',
|
||
problem: 'Частые обрывы связи',
|
||
responsible: 'Ерлан Нурбаев',
|
||
deadline: '2026-08-29',
|
||
status: 'Новые'
|
||
},
|
||
{
|
||
id: 6,
|
||
date: '2026-08-17',
|
||
name: 'Гүлмира Ибраева',
|
||
contact: '+7 (707) 678-90-12',
|
||
problem: 'Некорректные показания счётчика',
|
||
responsible: 'Гүлнұр Иманова',
|
||
deadline: '2026-08-24',
|
||
status: 'В работе'
|
||
},
|
||
{
|
||
id: 7,
|
||
date: '2026-08-15',
|
||
name: 'Ермек Байтасов',
|
||
contact: '+7 (776) 789-01-23',
|
||
problem: 'Подключение нового абонента',
|
||
responsible: 'Марат Серік',
|
||
deadline: '2026-08-22',
|
||
status: 'Выполнены'
|
||
},
|
||
{
|
||
id: 8,
|
||
date: '2026-08-16',
|
||
name: 'Зауре Алимова',
|
||
contact: '+7 (708) 890-12-34',
|
||
problem: 'Ошибка при авторизации в приложении',
|
||
responsible: 'Айдана Оразалы',
|
||
deadline: '2026-08-23',
|
||
status: 'В работе'
|
||
}
|
||
];
|
||
|
||
// Хранилище
|
||
let appeals = [];
|
||
let changeLog = [];
|
||
let currentFilter = 'all';
|
||
let searchQuery = '';
|
||
|
||
// Инициализация
|
||
function init() {
|
||
loadData();
|
||
setupEventListeners();
|
||
render();
|
||
}
|
||
|
||
// Загрузка данных из localStorage
|
||
function loadData() {
|
||
const stored = localStorage.getItem('appealsData');
|
||
if (stored) {
|
||
appeals = JSON.parse(stored);
|
||
} else {
|
||
appeals = [...DEMO_DATA];
|
||
saveData();
|
||
}
|
||
|
||
const logStored = localStorage.getItem('appealsLog');
|
||
if (logStored) {
|
||
changeLog = JSON.parse(logStored);
|
||
} else {
|
||
changeLog = [];
|
||
saveLog();
|
||
}
|
||
}
|
||
|
||
// Сохранение данных
|
||
function saveData() {
|
||
localStorage.setItem('appealsData', JSON.stringify(appeals));
|
||
}
|
||
|
||
function saveLog() {
|
||
localStorage.setItem('appealsLog', JSON.stringify(changeLog));
|
||
}
|
||
|
||
// Добавление записи в журнал
|
||
function addToLog(action, who) {
|
||
const now = new Date();
|
||
const timestamp = now.toLocaleString('ru-RU', {
|
||
day: '2-digit',
|
||
month: '2-digit',
|
||
year: 'numeric',
|
||
hour: '2-digit',
|
||
minute: '2-digit'
|
||
});
|
||
changeLog.unshift({
|
||
when: timestamp,
|
||
what: action,
|
||
who: who
|
||
});
|
||
// Храним только последние 50 записей
|
||
if (changeLog.length > 50) {
|
||
changeLog = changeLog.slice(0, 50);
|
||
}
|
||
saveLog();
|
||
}
|
||
|
||
// Проверка на просрочку
|
||
function isOverdue(item) {
|
||
if (item.status === 'Выполнены') return false;
|
||
const today = new Date();
|
||
today.setHours(0, 0, 0, 0);
|
||
const deadline = new Date(item.deadline);
|
||
return deadline < today;
|
||
}
|
||
|
||
// Форматирование даты
|
||
function formatDate(dateStr) {
|
||
const [year, month, day] = dateStr.split('-');
|
||
return `${day}.${month}.${year}`;
|
||
}
|
||
|
||
// Фильтрация данных
|
||
function getFilteredData() {
|
||
let filtered = [...appeals];
|
||
|
||
// Фильтр по статусу
|
||
if (currentFilter !== 'all') {
|
||
filtered = filtered.filter(item => item.status === currentFilter);
|
||
}
|
||
|
||
// Поиск
|
||
if (searchQuery.trim()) {
|
||
const query = searchQuery.toLowerCase();
|
||
filtered = filtered.filter(item =>
|
||
item.name.toLowerCase().includes(query) ||
|
||
item.contact.toLowerCase().includes(query) ||
|
||
item.problem.toLowerCase().includes(query) ||
|
||
item.responsible.toLowerCase().includes(query) ||
|
||
item.status.toLowerCase().includes(query)
|
||
);
|
||
}
|
||
|
||
return filtered;
|
||
}
|
||
|
||
// Подсчёт статистики
|
||
function updateStats() {
|
||
const total = appeals.length;
|
||
const newCount = appeals.filter(i => i.status === 'Новые').length;
|
||
const inProgress = appeals.filter(i => i.status === 'В работе').length;
|
||
const completed = appeals.filter(i => i.status === 'Выполнены').length;
|
||
const overdue = appeals.filter(i => isOverdue(i)).length;
|
||
|
||
document.getElementById('stat-total').textContent = total;
|
||
document.getElementById('stat-new').textContent = newCount;
|
||
document.getElementById('stat-in-progress').textContent = inProgress;
|
||
document.getElementById('stat-completed').textContent = completed;
|
||
document.getElementById('stat-overdue').textContent = overdue;
|
||
}
|
||
|
||
// Рендер таблицы
|
||
function renderTable() {
|
||
const tbody = document.getElementById('table-body');
|
||
const filtered = getFilteredData();
|
||
|
||
if (filtered.length === 0) {
|
||
tbody.innerHTML = '<tr><td colspan="9" style="text-align:center;padding:40px;color:#999;">Нет данных</td></tr>';
|
||
return;
|
||
}
|
||
|
||
tbody.innerHTML = filtered.map(item => {
|
||
const overdue = isOverdue(item);
|
||
let statusClass = 'status-new';
|
||
if (item.status === 'В работе') statusClass = 'status-in-progress';
|
||
if (item.status === 'Выполнены') statusClass = 'status-completed';
|
||
|
||
return `
|
||
<tr>
|
||
<td>${formatDate(item.date)}</td>
|
||
<td>${escapeHtml(item.name)}</td>
|
||
<td>${escapeHtml(item.contact)}</td>
|
||
<td>${escapeHtml(item.problem)}</td>
|
||
<td>${escapeHtml(item.responsible)}</td>
|
||
<td>${formatDate(item.deadline)}</td>
|
||
<td><span class="status-badge ${statusClass}">${item.status}</span></td>
|
||
<td>${overdue ? '<span class="overdue-badge">Просрочено</span>' : '—'}</td>
|
||
<td>
|
||
<div class="action-buttons">
|
||
<button class="btn btn-small btn-edit" onclick="openEditModal(${item.id})">Редактировать</button>
|
||
<button class="btn btn-small btn-delete" onclick="deleteAppeal(${item.id})">Удалить</button>
|
||
</div>
|
||
</td>
|
||
</tr>
|
||
`;
|
||
}).join('');
|
||
}
|
||
|
||
// Рендер журнала
|
||
function renderLog() {
|
||
const tbody = document.getElementById('log-body');
|
||
|
||
if (changeLog.length === 0) {
|
||
tbody.innerHTML = '<tr><td colspan="3" style="text-align:center;padding:20px;color:#999;">Журнал пуст</td></tr>';
|
||
return;
|
||
}
|
||
|
||
tbody.innerHTML = changeLog.slice(0, 20).map(log => `
|
||
<tr>
|
||
<td>${escapeHtml(log.when)}</td>
|
||
<td>${escapeHtml(log.what)}</td>
|
||
<td>${escapeHtml(log.who)}</td>
|
||
</tr>
|
||
`).join('');
|
||
}
|
||
|
||
// Полный рендер
|
||
function render() {
|
||
updateStats();
|
||
renderTable();
|
||
renderLog();
|
||
}
|
||
|
||
// Настройка обработчиков
|
||
function setupEventListeners() {
|
||
// Чипсы статусов
|
||
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('search-input').addEventListener('input', (e) => {
|
||
searchQuery = e.target.value;
|
||
renderTable();
|
||
});
|
||
|
||
// Добавление
|
||
document.getElementById('btn-add').addEventListener('click', () => {
|
||
openAddModal();
|
||
});
|
||
|
||
// Выгрузка
|
||
document.getElementById('btn-export').addEventListener('click', exportToCsv);
|
||
|
||
// Модальное окно
|
||
document.getElementById('modal-close').addEventListener('click', closeModal);
|
||
document.getElementById('btn-cancel').addEventListener('click', closeModal);
|
||
document.getElementById('modal-overlay').addEventListener('click', (e) => {
|
||
if (e.target === document.getElementById('modal-overlay')) {
|
||
closeModal();
|
||
}
|
||
});
|
||
|
||
// Форма
|
||
document.getElementById('appeal-form').addEventListener('submit', handleFormSubmit);
|
||
}
|
||
|
||
// Открытие модального окна для добавления
|
||
function openAddModal() {
|
||
document.getElementById('modal-title').textContent = 'Добавить обращение';
|
||
document.getElementById('form-id').value = '';
|
||
document.getElementById('form-date').value = new Date().toISOString().split('T')[0];
|
||
document.getElementById('form-deadline').value = '';
|
||
document.getElementById('form-name').value = '';
|
||
document.getElementById('form-contact').value = '';
|
||
document.getElementById('form-problem').value = '';
|
||
document.getElementById('form-responsible').value = '';
|
||
document.getElementById('form-status').value = 'Новые';
|
||
|
||
document.getElementById('modal-overlay').classList.add('active');
|
||
}
|
||
|
||
// Открытие модального окна для редактирования
|
||
function openEditModal(id) {
|
||
const item = appeals.find(a => a.id === id);
|
||
if (!item) return;
|
||
|
||
document.getElementById('modal-title').textContent = 'Редактировать обращение';
|
||
document.getElementById('form-id').value = item.id;
|
||
document.getElementById('form-date').value = item.date;
|
||
document.getElementById('form-deadline').value = item.deadline;
|
||
document.getElementById('form-name').value = item.name;
|
||
document.getElementById('form-contact').value = item.contact;
|
||
document.getElementById('form-problem').value = item.problem;
|
||
document.getElementById('form-responsible').value = item.responsible;
|
||
document.getElementById('form-status').value = item.status;
|
||
|
||
document.getElementById('modal-overlay').classList.add('active');
|
||
}
|
||
|
||
// Закрытие модального окна
|
||
function closeModal() {
|
||
document.getElementById('modal-overlay').classList.remove('active');
|
||
}
|
||
|
||
// Обработка формы
|
||
function handleFormSubmit(e) {
|
||
e.preventDefault();
|
||
|
||
const id = document.getElementById('form-id').value;
|
||
const formData = {
|
||
date: document.getElementById('form-date').value,
|
||
name: document.getElementById('form-name').value,
|
||
contact: document.getElementById('form-contact').value,
|
||
problem: document.getElementById('form-problem').value,
|
||
responsible: document.getElementById('form-responsible').value,
|
||
deadline: document.getElementById('form-deadline').value,
|
||
status: document.getElementById('form-status').value
|
||
};
|
||
|
||
if (id) {
|
||
// Редактирование
|
||
const index = appeals.findIndex(a => a.id === parseInt(id));
|
||
if (index !== -1) {
|
||
const old = appeals[index];
|
||
appeals[index] = { ...formData, id: parseInt(id) };
|
||
addToLog(`Изменено обращение #${id}: "${old.name}"`, 'Пользователь');
|
||
}
|
||
} else {
|
||
// Добавление
|
||
const newId = Date.now();
|
||
appeals.unshift({ ...formData, id: newId });
|
||
addToLog(`Добавлено обращение #${newId}: "${formData.name}"`, 'Пользователь');
|
||
}
|
||
|
||
saveData();
|
||
closeModal();
|
||
render();
|
||
}
|
||
|
||
// Удаление
|
||
function deleteAppeal(id) {
|
||
const item = appeals.find(a => a.id === id);
|
||
if (!item) return;
|
||
|
||
if (confirm(`Вы уверены, что хотите удалить обращение "${item.name}"?`)) {
|
||
appeals = appeals.filter(a => a.id !== id);
|
||
addToLog(`Удалено обращение #${id}: "${item.name}"`, 'Пользователь');
|
||
saveData();
|
||
render();
|
||
}
|
||
}
|
||
|
||
// Выгрузка в CSV
|
||
function exportToCsv() {
|
||
const filtered = getFilteredData();
|
||
|
||
if (filtered.length === 0) {
|
||
alert('Нет данных для выгрузки');
|
||
return;
|
||
}
|
||
|
||
const headers = ['Дата обращения', 'Кто обратился', 'Контакт', 'Суть проблемы', 'Ответственный', 'Срок', 'Статус', 'Просрочка'];
|
||
const rows = filtered.map(item => [
|
||
formatDate(item.date),
|
||
item.name,
|
||
item.contact,
|
||
item.problem,
|
||
item.responsible,
|
||
formatDate(item.deadline),
|
||
item.status,
|
||
isOverdue(item) ? 'Да' : 'Нет'
|
||
]);
|
||
|
||
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 = `reestr_${new Date().toISOString().split('T')[0]}.csv`;
|
||
link.click();
|
||
|
||
addToLog('Выгружен реестр в Excel (CSV)', 'Пользователь');
|
||
}
|
||
|
||
// Экранирование HTML
|
||
function escapeHtml(text) {
|
||
const div = document.createElement('div');
|
||
div.textContent = text;
|
||
return div.innerHTML;
|
||
}
|
||
|
||
// Запуск
|
||
init();
|