document.addEventListener('DOMContentLoaded', () => { const createForm = document.getElementById('create-form'); const statusFilter = document.getElementById('status-filter'); const categoryFilter = document.getElementById('category-filter'); const searchInput = document.getElementById('search-input'); const openModalBtn = document.getElementById('open-modal-btn'); const modal = document.getElementById('create-ticket-modal'); const closeModalBtn = document.getElementById('close-modal-btn'); 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'); // Accordion elements const closedToggle = document.getElementById('closed-toggle'); const closedAccordion = document.getElementById('closed-accordion'); const closedCount = document.getElementById('closed-count'); let selectedPfpFile = null; let allTickets = []; let accordionOpen = false; // --- XSS Protection --- function escapeHtml(str) { if (!str) return ''; const div = document.createElement('div'); div.textContent = str; return div.innerHTML; } function formatStatusClass(status) { if (!status) return 'Unknown'; return status.split(' ').map(s => s.charAt(0).toUpperCase() + s.slice(1).toLowerCase()).join('-'); } // --- Dynamic Categories --- async function loadCategories() { try { const res = await fetch('/api/categories'); const categories = await res.json(); populateCategorySelects(categories); } catch (err) { console.error('Failed to load categories:', err); } } function populateCategorySelects(categories) { // Modal create form select const createSelect = document.getElementById('category'); if (createSelect) { createSelect.innerHTML = categories.map(c => ``).join(''); } // Filter select (keep "All Categories" as first option) if (categoryFilter) { categoryFilter.innerHTML = '' + categories.map(c => ``).join(''); } } // --- Accordion --- if (closedToggle) { closedToggle.addEventListener('click', () => { accordionOpen = !accordionOpen; closedAccordion.style.display = accordionOpen ? 'block' : 'none'; closedToggle.classList.toggle('open', accordionOpen); }); } // --- Ticket Modal --- function openModal() { if (modal) { const scrollBarWidth = window.innerWidth - document.documentElement.clientWidth; modal.style.display = 'flex'; document.body.style.overflow = 'hidden'; document.documentElement.style.overflow = 'hidden'; if (scrollBarWidth > 0) document.body.style.paddingRight = scrollBarWidth + 'px'; } } function closeModal() { if (modal) { modal.style.display = 'none'; document.body.style.overflow = ''; document.documentElement.style.overflow = ''; document.body.style.paddingRight = ''; } } if (openModalBtn) openModalBtn.addEventListener('click', openModal); if (closeModalBtn) closeModalBtn.addEventListener('click', closeModal); if (modal) { window.addEventListener('click', (e) => { if (e.target === modal) closeModal(); }); window.addEventListener('keydown', (e) => { if (e.key === 'Escape' && modal.style.display === 'flex') closeModal(); }); } // --- 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, 'user.jpg'); try { const res = await fetch('/api/upload-pfp', { method: 'POST', body: formData }); if (!res.ok) throw new Error('Upload failed'); alert('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 createTicket(ticket) { const res = await fetch('/api/tickets', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(ticket), }); if (!res.ok) throw new Error('Failed to create ticket'); return await res.json(); } function formatDate(dateStr) { return new Date(dateStr).toLocaleDateString(undefined, { year: 'numeric', month: 'short', day: 'numeric', }); } // --- Render --- function renderFilteredTickets() { const status = statusFilter?.value || 'all'; const category = categoryFilter?.value || 'all'; const search = searchInput?.value.trim().toLowerCase() || ''; const filtered = allTickets.filter(ticket => { const ticketStatus = (ticket.status || '').toLowerCase(); const ticketCategory = (ticket.category || '').toLowerCase(); const ticketTitle = (ticket.title || '').toLowerCase(); const ticketDescription = (ticket.description || '').toLowerCase(); const isClosed = ticketStatus === 'closed'; const matchesStatus = status === 'all' || ticketStatus === status.toLowerCase(); const matchesCategory = category === 'all' || ticketCategory === category.toLowerCase(); const matchesSearch = ticketTitle.includes(search) || ticketDescription.includes(search); return (isClosed || matchesStatus) && matchesCategory && matchesSearch; }); renderTickets(filtered); } function renderTickets(tickets) { const openContainer = document.getElementById('tickets-container'); const closedContainer = document.getElementById('closed-tickets-container'); openContainer.innerHTML = ''; closedContainer.innerHTML = ''; let closedTicketCount = 0; if (!tickets.length) { openContainer.innerHTML = `

No tickets found

Try changing your filters or create a new ticket

`; if (closedCount) closedCount.textContent = '0'; return; } tickets.sort((a, b) => new Date(b.created_at) - new Date(a.created_at)); tickets.forEach(ticket => { const statusSafe = formatStatusClass(ticket.status); const prioritySafe = ticket.priority || 'None'; const el = document.createElement('div'); el.className = `ticket status-${statusSafe.replace(/\s+/g, '-')}`; el.innerHTML = `

${escapeHtml(ticket.title)} #${ticket.id}

${escapeHtml(ticket.description)}

${formatDate(ticket.created_at)} ${ticket.total_time} min
${escapeHtml(ticket.status)}
${ticket.category ? `
${escapeHtml(ticket.category)}
` : ''}
${escapeHtml(prioritySafe)}
View Details
`; if ((ticket.status || '').toLowerCase() === 'closed') { closedContainer.appendChild(el); closedTicketCount++; } else { openContainer.appendChild(el); } }); if (closedCount) closedCount.textContent = closedTicketCount; } // --- Handlers --- let isSubmittingTicket = false; async function handleCreateTicket(e) { e.preventDefault(); if (isSubmittingTicket) return; isSubmittingTicket = true; const title = document.getElementById('title')?.value.trim(); const description = document.getElementById('description')?.value.trim(); const priority = document.getElementById('priority')?.value; const category = document.getElementById('category')?.value; if (!title || !description) { alert('Please fill in all required fields'); isSubmittingTicket = false; return; } const submitBtn = createForm.querySelector('.btn-primary'); submitBtn.disabled = true; try { await createTicket({ title, description, priority, category, requester: 'Laila', status: 'Open' }); createForm.reset(); closeModal(); await refreshTickets(); submitBtn.innerHTML = ' Created!'; submitBtn.style.backgroundColor = '#28a745'; setTimeout(() => { submitBtn.innerHTML = ' Create Ticket'; submitBtn.style.backgroundColor = ''; submitBtn.disabled = false; isSubmittingTicket = false; }, 2000); } catch { alert('Failed to create ticket'); submitBtn.disabled = false; isSubmittingTicket = false; } } async function refreshTickets() { allTickets = await fetchTickets(); renderFilteredTickets(); } // --- Init --- if (createForm) createForm.addEventListener('submit', handleCreateTicket); if (statusFilter) statusFilter.addEventListener('change', renderFilteredTickets); if (categoryFilter) categoryFilter.addEventListener('change', renderFilteredTickets); if (searchInput) searchInput.addEventListener('input', renderFilteredTickets); loadCategories().then(() => refreshTickets()); });