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 = '' + categories.map(c => ``).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 = ` ${ticket.id} ${escapeHtml(shortTitle)} ${escapeHtml(ticket.requester)} ${escapeHtml(ticket.category) || '-'} ${escapeHtml(ticket.status) || 'Unknown'} ${escapeHtml(ticket.priority) || 'None'} ${formatDate(ticket.created_at)} ${ticket.total_time} min `; if (ticket.status === 'Closed') closedRows.push(rowHTML); else openRows.push(rowHTML); }); ticketsBody.innerHTML = openRows.length ? openRows.join('') : `No open tickets found`; if (closedTicketsBody) { closedTicketsBody.innerHTML = closedRows.length ? closedRows.join('') : `No closed tickets`; } 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()); });