252 lines
9.6 KiB
JavaScript
252 lines
9.6 KiB
JavaScript
document.addEventListener('DOMContentLoaded', () => {
|
|
const ticketsBody = document.getElementById('tickets-body');
|
|
const closedTicketsBody = document.getElementById('closed-tickets-body');
|
|
const searchInput = document.getElementById('search-tickets');
|
|
const statusFilter = document.getElementById('status-filter');
|
|
const priorityFilter = document.getElementById('priority-filter');
|
|
const categoryFilter = document.getElementById('category-filter');
|
|
const statTotal = document.getElementById('total-tickets');
|
|
const statOpen = document.getElementById('open-tickets');
|
|
const statInProgress = document.getElementById('in-progress-tickets');
|
|
const statClosed = document.getElementById('closed-tickets');
|
|
const statTotalTime = document.getElementById('time-tickets');
|
|
|
|
// Accordion elements
|
|
const closedToggle = document.getElementById('closed-toggle');
|
|
const closedAccordion = document.getElementById('closed-accordion');
|
|
const closedCount = document.getElementById('closed-count');
|
|
|
|
// PFP elements
|
|
const updatePfpBtn = document.getElementById('update-pfp-btn');
|
|
const pfpModal = document.getElementById('update-pfp-modal');
|
|
const dropZone = document.getElementById('drop-zone');
|
|
const fileInput = document.getElementById('pfp-file-input');
|
|
const uploadPfpBtn = document.getElementById('upload-pfp-btn');
|
|
|
|
let selectedPfpFile = null;
|
|
let accordionOpen = false;
|
|
|
|
searchInput.addEventListener('input', refreshTickets);
|
|
statusFilter.addEventListener('change', refreshTickets);
|
|
priorityFilter.addEventListener('change', refreshTickets);
|
|
categoryFilter.addEventListener('change', refreshTickets);
|
|
|
|
// --- XSS Protection ---
|
|
function escapeHtml(str) {
|
|
if (!str) return '';
|
|
const div = document.createElement('div');
|
|
div.textContent = str;
|
|
return div.innerHTML;
|
|
}
|
|
|
|
function toTitleCaseWithDash(str) {
|
|
return str.split(' ').map(word => word.charAt(0).toUpperCase() + word.slice(1).toLowerCase()).join('-');
|
|
}
|
|
|
|
// --- Dynamic Categories ---
|
|
async function loadCategories() {
|
|
try {
|
|
const res = await fetch('/api/categories');
|
|
const categories = await res.json();
|
|
if (categoryFilter) {
|
|
categoryFilter.innerHTML = '<option value="all">All Categories</option>' +
|
|
categories.map(c => `<option value="${escapeHtml(c)}">${escapeHtml(c)}</option>`).join('');
|
|
}
|
|
} catch (err) {
|
|
console.error('Failed to load categories:', err);
|
|
}
|
|
}
|
|
|
|
// --- Accordion ---
|
|
if (closedToggle) {
|
|
closedToggle.addEventListener('click', () => {
|
|
accordionOpen = !accordionOpen;
|
|
closedAccordion.style.display = accordionOpen ? 'block' : 'none';
|
|
closedToggle.classList.toggle('open', accordionOpen);
|
|
});
|
|
}
|
|
|
|
// --- PFP Modal ---
|
|
function openPfpModal() {
|
|
if (pfpModal) {
|
|
const scrollBarWidth = window.innerWidth - document.documentElement.clientWidth;
|
|
pfpModal.style.display = 'flex';
|
|
document.body.style.overflow = 'hidden';
|
|
document.documentElement.style.overflow = 'hidden';
|
|
if (scrollBarWidth > 0) document.body.style.paddingRight = scrollBarWidth + 'px';
|
|
}
|
|
}
|
|
function closePfpModal() {
|
|
if (pfpModal) {
|
|
pfpModal.style.display = 'none';
|
|
document.body.style.overflow = '';
|
|
document.documentElement.style.overflow = '';
|
|
document.body.style.paddingRight = '';
|
|
}
|
|
}
|
|
if (updatePfpBtn) updatePfpBtn.addEventListener('click', openPfpModal);
|
|
if (pfpModal) {
|
|
window.addEventListener('click', (e) => { if (e.target === pfpModal) closePfpModal(); });
|
|
window.addEventListener('keydown', (e) => { if (e.key === 'Escape' && pfpModal.style.display === 'flex') closePfpModal(); });
|
|
}
|
|
|
|
// --- Drag & Drop ---
|
|
if (dropZone) {
|
|
dropZone.addEventListener('click', () => fileInput.click());
|
|
dropZone.addEventListener('dragover', (e) => { e.preventDefault(); dropZone.style.borderColor = '#10b981'; });
|
|
dropZone.addEventListener('dragleave', () => { dropZone.style.borderColor = '#4b5563'; });
|
|
dropZone.addEventListener('drop', (e) => {
|
|
e.preventDefault();
|
|
dropZone.style.borderColor = '#4b5563';
|
|
const file = e.dataTransfer.files[0];
|
|
if (file && file.type.startsWith('image/')) {
|
|
selectedPfpFile = file;
|
|
dropZone.querySelector('p').textContent = file.name;
|
|
}
|
|
});
|
|
}
|
|
if (fileInput) {
|
|
fileInput.addEventListener('change', (e) => {
|
|
const file = e.target.files[0];
|
|
if (file && file.type.startsWith('image/')) {
|
|
selectedPfpFile = file;
|
|
dropZone.querySelector('p').textContent = file.name;
|
|
}
|
|
});
|
|
}
|
|
if (uploadPfpBtn) {
|
|
uploadPfpBtn.addEventListener('click', async () => {
|
|
if (!selectedPfpFile) return alert('Please select an image first.');
|
|
const formData = new FormData();
|
|
formData.append('pfp', selectedPfpFile, 'admin.jpg');
|
|
try {
|
|
const res = await fetch('/api/upload-admin-pfp', { method: 'POST', body: formData });
|
|
if (!res.ok) throw new Error('Upload failed');
|
|
alert('Admin profile picture updated!');
|
|
closePfpModal();
|
|
window.location.reload();
|
|
} catch (err) {
|
|
console.error(err);
|
|
alert('Error uploading profile picture');
|
|
}
|
|
});
|
|
}
|
|
|
|
// --- API ---
|
|
async function fetchTickets() {
|
|
try {
|
|
const res = await fetch('/api/tickets');
|
|
return await res.json();
|
|
} catch (err) {
|
|
console.error('Error fetching tickets:', err);
|
|
return [];
|
|
}
|
|
}
|
|
|
|
async function deleteTicket(id) {
|
|
try {
|
|
const res = await fetch(`/api/tickets/${id}`, {
|
|
method: 'DELETE',
|
|
headers: { 'Content-Type': 'application/json' },
|
|
body: JSON.stringify({ actor: 'Admin' }),
|
|
});
|
|
return await res.json();
|
|
} catch (err) {
|
|
console.error('Error deleting ticket:', err);
|
|
}
|
|
}
|
|
|
|
async function refreshTickets() {
|
|
const tickets = await fetchTickets();
|
|
renderTickets(tickets);
|
|
updateStats(tickets);
|
|
}
|
|
|
|
function renderTickets(tickets) {
|
|
const status = statusFilter.value;
|
|
const priority = priorityFilter.value;
|
|
const category = categoryFilter.value;
|
|
const searchTerm = searchInput.value.toLowerCase();
|
|
|
|
const filtered = tickets
|
|
.filter(t =>
|
|
(status === 'all' || t.status === status) &&
|
|
(priority === 'all' || t.priority === priority) &&
|
|
(category === 'all' || t.category === category) &&
|
|
(t.title.toLowerCase().includes(searchTerm) ||
|
|
t.description.toLowerCase().includes(searchTerm) ||
|
|
t.requester.toLowerCase().includes(searchTerm))
|
|
)
|
|
.sort((a, b) => new Date(b.created_at) - new Date(a.created_at));
|
|
|
|
const openRows = [];
|
|
const closedRows = [];
|
|
|
|
filtered.forEach(ticket => {
|
|
const shortTitle = ticket.title.length > 50 ? ticket.title.slice(0, 50) + '...' : ticket.title;
|
|
const statusClass = toTitleCaseWithDash(ticket.status || 'Unknown');
|
|
const priorityClass = toTitleCaseWithDash(ticket.priority || 'None');
|
|
|
|
const rowHTML = `
|
|
<tr>
|
|
<td>${ticket.id}</td>
|
|
<td class="ticket-title">${escapeHtml(shortTitle)}</td>
|
|
<td>${escapeHtml(ticket.requester)}</td>
|
|
<td>${escapeHtml(ticket.category) || '-'}</td>
|
|
<td><span class="status-badge status-${statusClass}">${escapeHtml(ticket.status) || 'Unknown'}</span></td>
|
|
<td><span class="priority-badge priority-${priorityClass}">${escapeHtml(ticket.priority) || 'None'}</span></td>
|
|
<td>${formatDate(ticket.created_at)}</td>
|
|
<td>${ticket.total_time} min</td>
|
|
<td class="table-actions">
|
|
<button class="table-btn view-btn" data-id="${ticket.id}"><i class="fas fa-eye"></i></button>
|
|
<button class="table-btn delete-btn" data-id="${ticket.id}"><i class="fas fa-trash-alt"></i></button>
|
|
</td>
|
|
</tr>`;
|
|
|
|
if (ticket.status === 'Closed') closedRows.push(rowHTML);
|
|
else openRows.push(rowHTML);
|
|
});
|
|
|
|
ticketsBody.innerHTML = openRows.length
|
|
? openRows.join('')
|
|
: `<tr><td colspan="9" class="empty-table">No open tickets found</td></tr>`;
|
|
|
|
if (closedTicketsBody) {
|
|
closedTicketsBody.innerHTML = closedRows.length
|
|
? closedRows.join('')
|
|
: `<tr><td colspan="9" class="empty-table">No closed tickets</td></tr>`;
|
|
}
|
|
|
|
if (closedCount) closedCount.textContent = closedRows.length;
|
|
|
|
document.querySelectorAll('.view-btn').forEach(btn =>
|
|
btn.addEventListener('click', () => {
|
|
window.location.href = `/ticketview/ticket-view-admin.html?id=${btn.dataset.id}`;
|
|
})
|
|
);
|
|
document.querySelectorAll('.delete-btn').forEach(btn =>
|
|
btn.addEventListener('click', () => {
|
|
if (confirm('Are you sure you want to delete this ticket?')) {
|
|
deleteTicket(btn.dataset.id).then(refreshTickets);
|
|
}
|
|
})
|
|
);
|
|
}
|
|
|
|
function updateStats(tickets) {
|
|
statTotal.textContent = tickets.length;
|
|
statOpen.textContent = tickets.filter(t => t.status === 'Open').length;
|
|
statInProgress.textContent = tickets.filter(t => t.status === 'In Progress').length;
|
|
statClosed.textContent = tickets.filter(t => t.status === 'Closed').length;
|
|
const totalTime = tickets.reduce((sum, t) => sum + (t.total_time || 0), 0);
|
|
statTotalTime.textContent = `${totalTime} min`;
|
|
}
|
|
|
|
function formatDate(date) {
|
|
return new Date(date).toLocaleDateString(undefined, {
|
|
year: 'numeric', month: 'short', day: 'numeric',
|
|
});
|
|
}
|
|
|
|
loadCategories().then(() => refreshTickets());
|
|
}); |