Support Ticket System - Html Template Free

.ticket-table tr:last-child td border-bottom: none;

/* header area */ .header display: flex; flex-wrap: wrap; justify-content: space-between; align-items: center; margin-bottom: 2rem; gap: 1rem;

.stat-card background: white; border-radius: 24px; padding: 1.2rem 1.5rem; box-shadow: 0 4px 12px rgba(0,0,0,0.03); transition: 0.2s; border: 1px solid #e2e8f0; display: flex; align-items: center; justify-content: space-between;

.close-modal background: #eef2ff; border: none; padding: 8px 18px; border-radius: 40px; cursor: pointer; support ticket system html template free

// current filters & search let currentStatusFilter = "all"; let currentSearchQuery = "";

.priority.high background: #fee2e2; color: #b91c1c;

.priority.medium background: #fff3e3; color: #c2410c; "In progress" : ticket

// Helper: update stats (open, in-progress, resolved, total) function updateStats() const total = tickets.length; const open = tickets.filter(t => t.status === "open").length; const inProgress = tickets.filter(t => t.status === "in-progress").length; const resolved = tickets.filter(t => t.status === "resolved").length; statsGrid.innerHTML = ` <div class="stat-card"><div class="stat-info"><h3>Total tickets</h3><div class="stat-number">$total</div></div><div class="stat-icon"><i class="fas fa-ticket"></i></div></div> <div class="stat-card"><div class="stat-info"><h3>Open</h3><div class="stat-number">$open</div></div><div class="stat-icon"><i class="fas fa-inbox"></i></div></div> <div class="stat-card"><div class="stat-info"><h3>In progress</h3><div class="stat-number">$inProgress</div></div><div class="stat-icon"><i class="fas fa-spinner"></i></div></div> <div class="stat-card"><div class="stat-info"><h3>Resolved</h3><div class="stat-number">$resolved</div></div><div class="stat-icon"><i class="fas fa-check-circle"></i></div></div> `;

/* filter bar */ .filter-bar background: white; border-radius: 20px; padding: 0.8rem 1.5rem; margin-bottom: 1.8rem; display: flex; flex-wrap: wrap; align-items: center; justify-content: space-between; gap: 1rem; border: 1px solid #e2e8f0;

.ticket-table width: 100%; border-collapse: collapse; min-width: 680px; span class="priority $priorityClass"&gt

.priority font-size: 0.7rem; font-weight: 700; padding: 2px 10px; border-radius: 40px; background: #f1f5f9; display: inline-block;

.stat-icon font-size: 2.2rem; color: #3b82f6; opacity: 0.7;

.btn-outline background: transparent; border: 1px solid #cbd5e1; color: #1e293b; padding: 10px 20px; border-radius: 40px; font-weight: 500; cursor: pointer; transition: 0.2s; display: inline-flex; align-items: center; gap: 8px;

// render tickets based on filters & search function renderTickets() let filtered = [...tickets]; // status filter if (currentStatusFilter !== "all") filtered = filtered.filter(t => t.status === currentStatusFilter); // search filter (ID or subject) if (currentSearchQuery.trim() !== "") t.subject.toLowerCase().includes(query)); // sort by id desc (newest first) filtered.sort((a,b) => b.id.localeCompare(a.id)); if (filtered.length === 0) tbody.innerHTML = `<tr><td colspan="6" style="text-align:center; padding: 2rem;">No tickets found. Create a new one! <i class="fas fa-smile-wink"></i></td></tr>`; return; tbody.innerHTML = filtered.map(ticket => let statusClass = ""; let statusIcon = ""; if (ticket.status === "open") statusClass = "open"; statusIcon = "fa-circle-info"; else if (ticket.status === "in-progress") statusClass = "in-progress"; statusIcon = "fa-arrows-spin"; else statusClass = "resolved"; statusIcon = "fa-circle-check"; let priorityClass = ""; if (ticket.priority === "High") priorityClass = "high"; else if (ticket.priority === "Medium") priorityClass = "medium"; else priorityClass = "low"; return ` <tr data-id="$ticket.id"> <td style="font-weight:500;">$ticket.id</td> <td><div class="ticket-subject"><i class="fas fa-message"></i> $escapeHtml(ticket.subject)</div></td> <td><span class="status-badge $statusClass"><i class="fas $statusIcon"></i> $ticket.status === "in-progress" ? "In progress" : ticket.status.charAt(0).toUpperCase() + ticket.status.slice(1)</span></td> <td><span class="priority $priorityClass">$ticket.priority</span></td> <td style="font-size:0.8rem;">$ticket.createdAt</td> <td class="action-icons"> <i class="fas fa-eye" title="View details" data-id="$ticket.id" data-action="view"></i> <i class="fas fa-pen" title="Change status" data-id="$ticket.id" data-action="edit"></i> <i class="fas fa-trash-alt" title="Delete" data-id="$ticket.id" data-action="delete"></i> </td> </tr> `; ).join(""); // attach event listeners to action icons after render document.querySelectorAll(".action-icons i").forEach(icon => icon.addEventListener("click", (e) => e.stopPropagation(); const ticketId = icon.getAttribute("data-id"); const action = icon.getAttribute("data-action"); if (action === "delete") if (confirm("Are you sure you want to delete this ticket?")) tickets = tickets.filter(t => t.id !== ticketId); updateStats(); renderTickets(); else if (action === "view") const ticket = tickets.find(t => t.id === ticketId); if (ticket) alert(`📋 Ticket $ticket.id\nSubject: $ticket.subject\nStatus: $ticket.status\nPriority: $ticket.priority\nCreated: $ticket.createdAt\nDescription: $ticket.description `); else if (action === "edit") // quick status toggle: cycle statuses open -> in-progress -> resolved const ticket = tickets.find(t => t.id === ticketId); if (ticket) if (ticket.status === "open") ticket.status = "in-progress"; else if (ticket.status === "in-progress") ticket.status = "resolved"; else ticket.status = "open"; updateStats(); renderTickets(); ); ); // simple XSS prevention function escapeHtml(str) return str.replace(/[&<>]/g, function(m) if(m === '&') return '&'; if(m === '<') return '<'; if(m === '>') return '>'; return m; ).replace(/[\uD800-\uDBFF][\uDC00-\uDFFF]/g, function(c) return c; ); // add new ticket function addTicket(subject, priority, description) if (!subject.trim()) return false; const newId = generateTicketId(); const today = new Date().toISOString().slice(0,10); const newTicket = ; tickets.push(newTicket); updateStats(); renderTickets(); return true; // modal logic function openModal() modal.style.display = "flex"; function closeModal() modal.style.display = "none"; document.getElementById("ticketForm").reset(); newTicketBtn.addEventListener("click", openModal); closeModalBtn.addEventListener("click", closeModal); window.addEventListener("click", (e) => if (e.target === modal) closeModal(); ); ticketForm.addEventListener("submit", (e) => e.preventDefault(); const subject = document.getElementById("ticketSubject").value; const priority = document.getElementById("ticketPriority").value; const desc = document.getElementById("ticketDesc").value; if (!subject.trim()) alert("Please enter a subject"); return; addTicket(subject, priority, desc); closeModal(); ); // filter buttons logic filterBtns.forEach(btn => btn.addEventListener("click", () => filterBtns.forEach(b => b.classList.remove("active")); btn.classList.add("active"); currentStatusFilter = btn.getAttribute("data-filter"); renderTickets(); ); ); // search input searchInput.addEventListener("input", (e) => currentSearchQuery = e.target.value; renderTickets(); ); // export as CSV (free feature) function exportToCSV() let csvRows = []; csvRows.push(["Ticket ID", "Subject", "Status", "Priority", "Created Date", "Description"]); for (const ticket of tickets) csvRows.push([ ticket.id, `"$ticket.subject.replace(/"/g, '""')"`, ticket.status, ticket.priority, ticket.createdAt, `"$"` ]); const csvContent = csvRows.map(row => row.join(",")).join("\n"); const blob = new Blob([csvContent], type: "text/csv;charset=utf-8;" ); const link = document.createElement("a"); const url = URL.createObjectURL(blob); link.href = url; link.setAttribute("download", "support_tickets_export.csv"); document.body.appendChild(link); link.click(); document.body.removeChild(link); URL.revokeObjectURL(url); exportBtn.addEventListener("click", exportToCSV); // initial render updateStats(); renderTickets(); </script> </body> </html>