324 lines
12 KiB
JavaScript
324 lines
12 KiB
JavaScript
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 => `<option value="${escapeHtml(c)}">${escapeHtml(c)}</option>`).join('');
|
|
}
|
|
// Filter select (keep "All Categories" as first option)
|
|
if (categoryFilter) {
|
|
categoryFilter.innerHTML = '<option value="all">All Categories</option>' +
|
|
categories.map(c => `<option value="${escapeHtml(c)}">${escapeHtml(c)}</option>`).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 = `
|
|
<div class="empty-state">
|
|
<i class="fas fa-inbox"></i>
|
|
<h3>No tickets found</h3>
|
|
<p>Try changing your filters or create a new ticket</p>
|
|
</div>`;
|
|
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 = `
|
|
<h3>${escapeHtml(ticket.title)} <span class="ticket-id">#${ticket.id}</span></h3>
|
|
<p class="description">${escapeHtml(ticket.description)}</p>
|
|
<div class="meta">
|
|
<span class="date"><i class="fas fa-calendar"></i> ${formatDate(ticket.created_at)}</span>
|
|
<span class="total-time"><i class="fas fa-clock"></i> ${ticket.total_time} min</span>
|
|
</div>
|
|
<div class="status-row">
|
|
<div class="status status-${statusSafe.replace(/\s+/g, '-')}">
|
|
<i class="fas fa-circle"></i> ${escapeHtml(ticket.status)}
|
|
</div>
|
|
${ticket.category ? `<div class="category-badge"><i class="fas fa-tag"></i> ${escapeHtml(ticket.category)}</div>` : ''}
|
|
<div class="priority priority-${prioritySafe.replace(/\s+/g, '-')}">
|
|
<i class="fas fa-flag"></i> ${escapeHtml(prioritySafe)}
|
|
</div>
|
|
</div>
|
|
<div class="ticket-actions">
|
|
<a href="/ticketview/ticket-view-user.html?id=${ticket.id}" class="action-btn view-btn">
|
|
<i class="fas fa-eye"></i> View Details
|
|
</a>
|
|
</div>
|
|
`;
|
|
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 = '<i class="fas fa-check"></i> Created!';
|
|
submitBtn.style.backgroundColor = '#28a745';
|
|
setTimeout(() => {
|
|
submitBtn.innerHTML = '<i class="fas fa-paper-plane"></i> 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());
|
|
}); |