// Демо-данные: 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 = '
| Нет данных |
';
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 `
| ${formatDate(item.date)} |
${escapeHtml(item.name)} |
${escapeHtml(item.contact)} |
${escapeHtml(item.problem)} |
${escapeHtml(item.responsible)} |
${formatDate(item.deadline)} |
${item.status} |
${overdue ? 'Просрочено' : '—'} |
|
`;
}).join('');
}
// Рендер журнала
function renderLog() {
const tbody = document.getElementById('log-body');
if (changeLog.length === 0) {
tbody.innerHTML = '| Журнал пуст |
';
return;
}
tbody.innerHTML = changeLog.slice(0, 20).map(log => `
| ${escapeHtml(log.when)} |
${escapeHtml(log.what)} |
${escapeHtml(log.who)} |
`).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();