254 lines
8.3 KiB
JavaScript
254 lines
8.3 KiB
JavaScript
let appeals = [];
|
||
let currentFilter = 'all';
|
||
let currentSort = 'date';
|
||
let editingId = null;
|
||
|
||
const searchInput = document.getElementById('searchInput');
|
||
const appealsTable = document.getElementById('appealsTable');
|
||
const emptyState = document.getElementById('emptyState');
|
||
const drawer = document.getElementById('appealDrawer');
|
||
const drawerOverlay = document.getElementById('drawerOverlay');
|
||
const drawerTitle = document.getElementById('drawerTitle');
|
||
const appealForm = document.getElementById('appealForm');
|
||
const themeToggle = document.getElementById('themeToggle');
|
||
const toastContainer = document.getElementById('toastContainer');
|
||
|
||
const kpiTotal = document.getElementById('kpiTotal');
|
||
const kpiNew = document.getElementById('kpiNew');
|
||
const kpiDuplicate = document.getElementById('kpiDuplicate');
|
||
const kpiResolved = document.getElementById('kpiResolved');
|
||
|
||
document.addEventListener('DOMContentLoaded', () => {
|
||
loadAppeals();
|
||
setupEventListeners();
|
||
});
|
||
|
||
function setupEventListeners() {
|
||
document.getElementById('newAppealBtn').addEventListener('click', openNewDrawer);
|
||
document.getElementById('emptyNewBtn').addEventListener('click', openNewDrawer);
|
||
document.getElementById('closeDrawer').addEventListener('click', closeDrawer);
|
||
document.getElementById('cancelBtn').addEventListener('click', closeDrawer);
|
||
document.getElementById('saveBtn').addEventListener('click', saveAppeal);
|
||
document.getElementById('drawerOverlay').addEventListener('click', closeDrawer);
|
||
|
||
themeToggle.addEventListener('click', toggleTheme);
|
||
searchInput.addEventListener('input', renderAppeals);
|
||
document.getElementById('statusFilter').addEventListener('change', renderAppeals);
|
||
|
||
document.querySelectorAll('[data-filter]').forEach(link => {
|
||
link.addEventListener('click', (e) => {
|
||
e.preventDefault();
|
||
document.querySelectorAll('[data-filter]').forEach(l => l.classList.remove('is-active'));
|
||
link.classList.add('is-active');
|
||
currentFilter = link.dataset.filter;
|
||
renderAppeals();
|
||
});
|
||
});
|
||
|
||
document.querySelectorAll('[data-sort]').forEach(btn => {
|
||
btn.addEventListener('click', () => {
|
||
document.querySelectorAll('[data-sort]').forEach(b => b.classList.remove('is-active'));
|
||
btn.classList.add('is-active');
|
||
currentSort = btn.dataset.sort;
|
||
renderAppeals();
|
||
});
|
||
});
|
||
}
|
||
|
||
async function loadAppeals() {
|
||
try {
|
||
const res = await fetch('api/appeals');
|
||
appeals = await res.json();
|
||
renderAppeals();
|
||
} catch (err) {
|
||
console.error('Ошибка загрузки:', err);
|
||
showToast('Не удалось загрузить обращения');
|
||
}
|
||
}
|
||
|
||
function renderAppeals() {
|
||
const searchTerm = searchInput.value.toLowerCase();
|
||
const statusFilter = document.getElementById('statusFilter').value;
|
||
|
||
let filtered = appeals.filter(appeal => {
|
||
const matchesSearch =
|
||
appeal.author.toLowerCase().includes(searchTerm) ||
|
||
appeal.text.toLowerCase().includes(searchTerm) ||
|
||
appeal.contact.toLowerCase().includes(searchTerm) ||
|
||
appeal.createdAt.includes(searchTerm);
|
||
|
||
const matchesFilter = currentFilter === 'all' ||
|
||
(currentFilter === 'new' && appeal.status === 'new') ||
|
||
(currentFilter === 'duplicate' && appeal.status === 'duplicate') ||
|
||
(currentFilter === 'resolved' && appeal.status === 'resolved');
|
||
|
||
const matchesStatus = !statusFilter || appeal.status === statusFilter;
|
||
|
||
return matchesSearch && matchesFilter && matchesStatus;
|
||
});
|
||
|
||
if (currentSort === 'date') {
|
||
filtered.sort((a, b) => new Date(b.createdAt) - new Date(a.createdAt));
|
||
} else if (currentSort === 'author') {
|
||
filtered.sort((a, b) => a.author.localeCompare(b.author));
|
||
}
|
||
|
||
if (filtered.length === 0) {
|
||
appealsTable.innerHTML = '';
|
||
emptyState.hidden = false;
|
||
} else {
|
||
emptyState.hidden = true;
|
||
appealsTable.innerHTML = filtered.map(appeal => `
|
||
<tr>
|
||
<td>${formatDate(appeal.createdAt)}</td>
|
||
<td>${escapeHtml(appeal.author)}</td>
|
||
<td class="appeal-text">${escapeHtml(appeal.text)}</td>
|
||
<td><span class="status-pill status-${appeal.status}">${getStatusText(appeal.status)}</span></td>
|
||
<td>
|
||
<button class="icon-btn" onclick="editAppeal(${appeal.id})" title="Редактировать">
|
||
<svg class="kt-icon" aria-hidden="true"><use href="#pencil"></use></svg>
|
||
</button>
|
||
</td>
|
||
</tr>
|
||
`).join('');
|
||
}
|
||
|
||
updateKPI();
|
||
}
|
||
|
||
function updateKPI() {
|
||
kpiTotal.textContent = appeals.length;
|
||
kpiNew.textContent = appeals.filter(a => a.status === 'new').length;
|
||
kpiDuplicate.textContent = appeals.filter(a => a.status === 'duplicate').length;
|
||
kpiResolved.textContent = appeals.filter(a => a.status === 'resolved').length;
|
||
}
|
||
|
||
function openNewDrawer() {
|
||
editingId = null;
|
||
drawerTitle.textContent = 'Новое обращение';
|
||
appealForm.reset();
|
||
document.getElementById('appealId').value = '';
|
||
openDrawer();
|
||
}
|
||
|
||
function openDrawer() {
|
||
drawer.classList.add('open');
|
||
drawerOverlay.classList.add('open');
|
||
document.body.style.overflow = 'hidden';
|
||
}
|
||
|
||
function closeDrawer() {
|
||
drawer.classList.remove('open');
|
||
drawerOverlay.classList.remove('open');
|
||
document.body.style.overflow = '';
|
||
}
|
||
|
||
async function saveAppeal() {
|
||
const author = document.getElementById('authorInput').value.trim();
|
||
const contact = document.getElementById('contactInput').value.trim();
|
||
const text = document.getElementById('textInput').value.trim();
|
||
const category = document.getElementById('categoryInput').value;
|
||
const comment = document.getElementById('commentInput').value.trim();
|
||
|
||
if (!author || !text) {
|
||
showToast('Заполните обязательные поля');
|
||
return;
|
||
}
|
||
|
||
try {
|
||
if (editingId) {
|
||
const res = await fetch(`api/appeals/${editingId}`, {
|
||
method: 'PUT',
|
||
headers: { 'Content-Type': 'application/json' },
|
||
body: JSON.stringify({ author, contact, text, category, comment })
|
||
});
|
||
|
||
if (res.ok) {
|
||
showToast('Обращение обновлено');
|
||
await loadAppeals();
|
||
}
|
||
} else {
|
||
const res = await fetch('api/appeals', {
|
||
method: 'POST',
|
||
headers: { 'Content-Type': 'application/json' },
|
||
body: JSON.stringify({ author, contact, text, category, comment, status: 'new' })
|
||
});
|
||
|
||
if (res.ok) {
|
||
showToast('Обращение добавлено');
|
||
await loadAppeals();
|
||
}
|
||
}
|
||
|
||
closeDrawer();
|
||
} catch (err) {
|
||
console.error('Ошибка сохранения:', err);
|
||
showToast('Не удалось сохранить обращение');
|
||
}
|
||
}
|
||
|
||
function editAppeal(id) {
|
||
const appeal = appeals.find(a => a.id === id);
|
||
if (!appeal) return;
|
||
|
||
editingId = id;
|
||
drawerTitle.textContent = 'Редактирование обращения';
|
||
document.getElementById('appealId').value = appeal.id;
|
||
document.getElementById('authorInput').value = appeal.author;
|
||
document.getElementById('contactInput').value = appeal.contact;
|
||
document.getElementById('textInput').value = appeal.text;
|
||
document.getElementById('categoryInput').value = appeal.category || '';
|
||
document.getElementById('commentInput').value = appeal.comment || '';
|
||
|
||
openDrawer();
|
||
}
|
||
|
||
function toggleTheme() {
|
||
const html = document.documentElement;
|
||
const current = html.getAttribute('data-theme');
|
||
const next = current === 'dark' ? 'light' : current === 'light' ? 'auto' : 'dark';
|
||
html.setAttribute('data-theme', next);
|
||
|
||
const icon = themeToggle.querySelector('use');
|
||
icon.setAttribute('href', next === 'dark' ? '#moon' : '#sun');
|
||
}
|
||
|
||
function showToast(message) {
|
||
const toast = document.createElement('div');
|
||
toast.className = 'toast';
|
||
toast.textContent = message;
|
||
toastContainer.appendChild(toast);
|
||
|
||
setTimeout(() => {
|
||
toast.remove();
|
||
}, 3000);
|
||
}
|
||
|
||
function formatDate(isoString) {
|
||
const date = new Date(isoString);
|
||
return date.toLocaleDateString('ru-RU', {
|
||
day: '2-digit',
|
||
month: '2-digit',
|
||
year: '2-digit',
|
||
hour: '2-digit',
|
||
minute: '2-digit'
|
||
});
|
||
}
|
||
|
||
function getStatusText(status) {
|
||
const texts = {
|
||
new: 'Новое',
|
||
duplicate: 'Повторное',
|
||
resolved: 'Решено'
|
||
};
|
||
return texts[status] || status;
|
||
}
|
||
|
||
function escapeHtml(text) {
|
||
const div = document.createElement('div');
|
||
div.textContent = text;
|
||
return div.innerHTML;
|
||
}
|
||
|
||
window.editAppeal = editAppeal;
|