Files
Ticketsystem/public/admin-script.js
T

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());
});