reestr-obrascheniy-abonentov/script.js

433 lines
13 KiB
JavaScript
Raw 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.

// Демо-данные: 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();