Spaces:
Runtime error
Runtime error
| {% extends "base.html" %} | |
| {% block title %}My Support Tickets - Too Many Cables{% endblock %} | |
| {% block description %}Manage your support tickets and track the status of your requests with Too Many Cables customer service.{% endblock %} | |
| {% block content %} | |
| <div class="tickets-container"> | |
| <div class="page-header"> | |
| <h1 class="page-title">My Support Tickets</h1> | |
| <p class="page-subtitle">Track and manage your support requests</p> | |
| </div> | |
| <!-- User must be logged in to view tickets --> | |
| <div id="auth-required" class="auth-message" style="display: none;"> | |
| <div class="glass-card"> | |
| <h3>Login Required</h3> | |
| <p>Please log in to view and manage your support tickets.</p> | |
| <button class="btn btn-primary" onclick="showLoginModal()">Login</button> | |
| </div> | |
| </div> | |
| <!-- Main tickets interface --> | |
| <div id="tickets-main" style="display: none;"> | |
| <!-- Create New Ticket Section --> | |
| <div class="glass-card ticket-creation"> | |
| <h3>Create New Ticket</h3> | |
| <form id="create-ticket-form"> | |
| <div class="form-group"> | |
| <label for="ticket-subject">Subject</label> | |
| <input type="text" id="ticket-subject" name="subject" required | |
| placeholder="Brief description of your issue"> | |
| </div> | |
| <div class="form-group"> | |
| <label for="ticket-description">Description</label> | |
| <textarea id="ticket-description" name="description" required | |
| placeholder="Please provide detailed information about your issue" | |
| rows="4"></textarea> | |
| </div> | |
| <div class="form-row"> | |
| <div class="form-group"> | |
| <label for="ticket-priority">Priority</label> | |
| <select id="ticket-priority" name="priority"> | |
| <option value="low">Low</option> | |
| <option value="medium" selected>Medium</option> | |
| <option value="high">High</option> | |
| <option value="urgent">Urgent</option> | |
| </select> | |
| </div> | |
| <div class="form-group"> | |
| <label for="ticket-category">Category</label> | |
| <select id="ticket-category" name="category"> | |
| <option value="">Auto-detect</option> | |
| </select> | |
| </div> | |
| </div> | |
| <button type="submit" class="btn btn-primary">Create Ticket</button> | |
| </form> | |
| </div> | |
| <!-- Tickets List --> | |
| <div class="glass-card tickets-list"> | |
| <div class="tickets-header"> | |
| <h3>Your Tickets</h3> | |
| <div class="tickets-filters"> | |
| <select id="status-filter"> | |
| <option value="">All Status</option> | |
| <option value="open">Open</option> | |
| <option value="in_progress">In Progress</option> | |
| <option value="resolved">Resolved</option> | |
| <option value="closed">Closed</option> | |
| </select> | |
| </div> | |
| </div> | |
| <div id="tickets-loading" class="loading-state"> | |
| <div class="spinner"></div> | |
| <p>Loading your tickets...</p> | |
| </div> | |
| <div id="tickets-empty" class="empty-state" style="display: none;"> | |
| <h4>No tickets yet</h4> | |
| <p>When you create support tickets, they'll appear here.</p> | |
| </div> | |
| <div id="tickets-container"></div> | |
| </div> | |
| </div> | |
| </div> | |
| <!-- Ticket Detail Modal --> | |
| <div id="ticket-modal" class="modal" style="display: none;"> | |
| <div class="modal-content glass-card"> | |
| <div class="modal-header"> | |
| <h3 id="ticket-modal-title">Ticket Details</h3> | |
| <button class="modal-close" onclick="closeTicketModal()">×</button> | |
| </div> | |
| <div class="modal-body"> | |
| <div id="ticket-details"></div> | |
| <!-- SLA and Escalation Status --> | |
| <div id="sla-status" class="sla-section" style="display: none;"></div> | |
| <!-- Escalation Actions --> | |
| <div id="escalation-actions" class="escalation-section" style="display: none;"> | |
| <h4>Escalation</h4> | |
| <button class="btn btn-warning" onclick="escalateTicket()">Request Escalation</button> | |
| <p style="font-size: 0.9rem; color: rgba(0,0,0,0.7); margin-top: 0.5rem;"> | |
| Request priority escalation if your issue needs immediate attention. | |
| </p> | |
| </div> | |
| <!-- Add Update Form --> | |
| <div class="ticket-update-form"> | |
| <h4>Add Update</h4> | |
| <form id="update-ticket-form"> | |
| <input type="hidden" id="update-ticket-id"> | |
| <div class="form-group"> | |
| <textarea id="update-message" placeholder="Add a comment or update..." | |
| rows="3" required></textarea> | |
| </div> | |
| <button type="submit" class="btn btn-primary">Add Update</button> | |
| </form> | |
| </div> | |
| </div> | |
| </div> | |
| </div> | |
| <style> | |
| .tickets-container { | |
| max-width: 1200px; | |
| margin: 0 auto; | |
| padding: 2rem; | |
| } | |
| .page-header { | |
| text-align: center; | |
| margin-bottom: 3rem; | |
| } | |
| .page-title { | |
| font-size: 2.5rem; | |
| font-weight: 700; | |
| color: white; | |
| text-shadow: 0 2px 10px rgba(0,0,0,0.3); | |
| margin-bottom: 0.5rem; | |
| } | |
| .page-subtitle { | |
| font-size: 1.1rem; | |
| color: rgba(255,255,255,0.9); | |
| font-weight: 300; | |
| } | |
| .glass-card { | |
| background: rgba(255, 255, 255, 0.15); | |
| backdrop-filter: blur(25px); | |
| -webkit-backdrop-filter: blur(25px); | |
| border-radius: 20px; | |
| border: 1px solid rgba(255, 255, 255, 0.2); | |
| padding: 2rem; | |
| margin-bottom: 2rem; | |
| box-shadow: 0 8px 32px rgba(31, 38, 135, 0.37); | |
| } | |
| .ticket-creation h3, | |
| .tickets-list h3 { | |
| color: white; | |
| margin-bottom: 1.5rem; | |
| font-weight: 600; | |
| } | |
| .form-group { | |
| margin-bottom: 1.5rem; | |
| } | |
| .form-row { | |
| display: grid; | |
| grid-template-columns: 1fr 1fr; | |
| gap: 1rem; | |
| } | |
| .form-group label { | |
| display: block; | |
| color: rgba(255,255,255,0.9); | |
| margin-bottom: 0.5rem; | |
| font-weight: 500; | |
| } | |
| .form-group input, | |
| .form-group textarea, | |
| .form-group select { | |
| width: 100%; | |
| padding: 0.75rem 1rem; | |
| border: 1px solid rgba(255,255,255,0.3); | |
| border-radius: 10px; | |
| background: rgba(255,255,255,0.1); | |
| color: white; | |
| font-size: 1rem; | |
| transition: all 0.3s ease; | |
| } | |
| .form-group input:focus, | |
| .form-group textarea:focus, | |
| .form-group select:focus { | |
| outline: none; | |
| border-color: rgba(255,255,255,0.6); | |
| background: rgba(255,255,255,0.2); | |
| box-shadow: 0 0 0 3px rgba(255,255,255,0.1); | |
| } | |
| .form-group input::placeholder, | |
| .form-group textarea::placeholder { | |
| color: rgba(255,255,255,0.6); | |
| } | |
| .btn { | |
| padding: 0.75rem 1.5rem; | |
| border: none; | |
| border-radius: 10px; | |
| font-weight: 600; | |
| text-decoration: none; | |
| display: inline-block; | |
| transition: all 0.3s ease; | |
| cursor: pointer; | |
| font-size: 1rem; | |
| } | |
| .btn-primary { | |
| background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); | |
| color: white; | |
| box-shadow: 0 4px 15px rgba(102, 126, 234, 0.4); | |
| } | |
| .btn-primary:hover { | |
| transform: translateY(-2px); | |
| box-shadow: 0 8px 25px rgba(102, 126, 234, 0.6); | |
| } | |
| .tickets-header { | |
| display: flex; | |
| justify-content: space-between; | |
| align-items: center; | |
| margin-bottom: 1.5rem; | |
| } | |
| .tickets-filters select { | |
| padding: 0.5rem 1rem; | |
| border: 1px solid rgba(255,255,255,0.3); | |
| border-radius: 8px; | |
| background: rgba(255,255,255,0.1); | |
| color: white; | |
| } | |
| .ticket-item { | |
| background: rgba(255,255,255,0.1); | |
| border-radius: 10px; | |
| padding: 1.5rem; | |
| margin-bottom: 1rem; | |
| border: 1px solid rgba(255,255,255,0.2); | |
| cursor: pointer; | |
| transition: all 0.3s ease; | |
| } | |
| .ticket-item:hover { | |
| background: rgba(255,255,255,0.2); | |
| transform: translateY(-2px); | |
| } | |
| .ticket-header { | |
| display: flex; | |
| justify-content: space-between; | |
| align-items: start; | |
| margin-bottom: 0.5rem; | |
| } | |
| .ticket-number { | |
| font-weight: 600; | |
| color: white; | |
| font-size: 1.1rem; | |
| } | |
| .ticket-status { | |
| padding: 0.25rem 0.75rem; | |
| border-radius: 20px; | |
| font-size: 0.85rem; | |
| font-weight: 500; | |
| text-transform: uppercase; | |
| } | |
| .status-open { background: #f39c12; color: white; } | |
| .status-in_progress { background: #3498db; color: white; } | |
| .status-resolved { background: #27ae60; color: white; } | |
| .status-closed { background: #95a5a6; color: white; } | |
| .ticket-subject { | |
| color: white; | |
| font-size: 1.1rem; | |
| margin-bottom: 0.5rem; | |
| font-weight: 500; | |
| } | |
| .ticket-meta { | |
| display: flex; | |
| justify-content: space-between; | |
| align-items: center; | |
| color: rgba(255,255,255,0.7); | |
| font-size: 0.9rem; | |
| } | |
| .ticket-category { | |
| background: rgba(255,255,255,0.2); | |
| padding: 0.25rem 0.5rem; | |
| border-radius: 15px; | |
| font-size: 0.8rem; | |
| } | |
| .loading-state, | |
| .empty-state { | |
| text-align: center; | |
| padding: 2rem; | |
| color: rgba(255,255,255,0.8); | |
| } | |
| .spinner { | |
| border: 3px solid rgba(255,255,255,0.3); | |
| border-top: 3px solid white; | |
| border-radius: 50%; | |
| width: 40px; | |
| height: 40px; | |
| animation: spin 1s linear infinite; | |
| margin: 0 auto 1rem; | |
| } | |
| @keyframes spin { | |
| 0% { transform: rotate(0deg); } | |
| 100% { transform: rotate(360deg); } | |
| } | |
| .modal { | |
| position: fixed; | |
| top: 0; | |
| left: 0; | |
| right: 0; | |
| bottom: 0; | |
| background: rgba(0,0,0,0.8); | |
| display: flex; | |
| justify-content: center; | |
| align-items: center; | |
| z-index: 2000; | |
| } | |
| .modal-content { | |
| max-width: 600px; | |
| width: 90%; | |
| max-height: 80vh; | |
| overflow-y: auto; | |
| margin: 2rem; | |
| } | |
| .modal-header { | |
| display: flex; | |
| justify-content: space-between; | |
| align-items: center; | |
| margin-bottom: 1.5rem; | |
| } | |
| .modal-close { | |
| background: none; | |
| border: none; | |
| color: #1a202c; /* dark close button for contrast on light modal */ | |
| font-size: 2rem; | |
| cursor: pointer; | |
| opacity: 0.8; | |
| transition: opacity 0.3s ease; | |
| } | |
| .modal-close:hover { | |
| opacity: 1; | |
| } | |
| /* Ensure modal header and title are dark for readability */ | |
| .modal-header h3, .modal-content h3 { | |
| color: #1a202c ; | |
| } | |
| .auth-message { | |
| text-align: center; | |
| padding: 3rem 2rem; | |
| } | |
| .auth-message h3 { | |
| color: white; | |
| margin-bottom: 1rem; | |
| } | |
| .auth-message p { | |
| color: rgba(255,255,255,0.8); | |
| margin-bottom: 2rem; | |
| } | |
| .ticket-update-form { | |
| margin-top: 2rem; | |
| padding-top: 2rem; | |
| border-top: 1px solid rgba(255,255,255,0.2); | |
| } | |
| .ticket-update-form h4 { | |
| color: white; | |
| margin-bottom: 1rem; | |
| } | |
| .escalation-badge { | |
| background: #e74c3c; | |
| color: white; | |
| padding: 0.25rem 0.5rem; | |
| border-radius: 12px; | |
| font-size: 0.75rem; | |
| font-weight: 600; | |
| margin-left: 0.5rem; | |
| } | |
| /* Ensure ticket modal content is readable: light card with dark text */ | |
| .modal-content { | |
| background: rgba(255,255,255,0.98) ; | |
| color: #1a202c ; | |
| border-radius: 12px; | |
| padding: 1.5rem; | |
| box-shadow: 0 10px 30px rgba(0,0,0,0.3); | |
| } | |
| /* Strong override: force dark text for modal and all children to avoid inheritance issues */ | |
| .modal-content.glass-card, | |
| .modal-content.glass-card * { | |
| color: #1a202c ; | |
| } | |
| /* Ensure headings and paragraphs within modal are readable */ | |
| .modal-content h1, | |
| .modal-content h2, | |
| .modal-content h3, | |
| .modal-content h4, | |
| .modal-content p, | |
| .modal-content label, | |
| .modal-content span, | |
| .modal-content div { | |
| color: #1a202c ; | |
| } | |
| .sla-section { | |
| background: rgba(255,255,255,0.1); | |
| border-radius: 10px; | |
| padding: 1.5rem; | |
| margin: 1.5rem 0; | |
| border-left: 4px solid #3498db; | |
| } | |
| .sla-section h4 { | |
| color: white; | |
| margin-bottom: 1rem; | |
| } | |
| .sla-metric { | |
| display: flex; | |
| justify-content: space-between; | |
| align-items: center; | |
| margin-bottom: 0.5rem; | |
| color: rgba(255,255,255,0.9); | |
| } | |
| .sla-status { | |
| padding: 0.25rem 0.75rem; | |
| border-radius: 15px; | |
| font-size: 0.85rem; | |
| font-weight: 500; | |
| } | |
| .sla-met { background: #27ae60; color: white; } | |
| .sla-warning { background: #f39c12; color: white; } | |
| .sla-breach { background: #e74c3c; color: white; } | |
| .escalation-section { | |
| background: rgba(255,255,255,0.1); | |
| border-radius: 10px; | |
| padding: 1.5rem; | |
| margin: 1.5rem 0; | |
| border-left: 4px solid #e74c3c; | |
| } | |
| .escalation-section h4 { | |
| color: white; | |
| margin-bottom: 1rem; | |
| } | |
| .btn-warning { | |
| background: linear-gradient(135deg, #f39c12 0%, #e67e22 100%); | |
| color: white; | |
| box-shadow: 0 4px 15px rgba(243, 156, 18, 0.4); | |
| } | |
| .btn-warning:hover { | |
| transform: translateY(-2px); | |
| box-shadow: 0 8px 25px rgba(243, 156, 18, 0.6); | |
| } | |
| @media (max-width: 768px) { | |
| .form-row { | |
| grid-template-columns: 1fr; | |
| } | |
| .tickets-header { | |
| flex-direction: column; | |
| gap: 1rem; | |
| align-items: stretch; | |
| } | |
| .ticket-header { | |
| flex-direction: column; | |
| gap: 0.5rem; | |
| } | |
| .ticket-meta { | |
| flex-direction: column; | |
| gap: 0.5rem; | |
| align-items: start; | |
| } | |
| } | |
| </style> | |
| <script> | |
| // Ticket management functionality | |
| let currentUser = null; | |
| let tickets = []; | |
| let categories = []; | |
| // Initialize page | |
| document.addEventListener('DOMContentLoaded', function() { | |
| checkAuthentication(); | |
| loadCategories(); | |
| }); | |
| function checkAuthentication() { | |
| fetch('/api/user') | |
| .then(response => response.json()) | |
| .then(data => { | |
| if (data.success && data.user) { | |
| currentUser = data.user; | |
| document.getElementById('auth-required').style.display = 'none'; | |
| document.getElementById('tickets-main').style.display = 'block'; | |
| loadTickets(); | |
| // Show tickets link in navigation | |
| const ticketsLink = document.getElementById('tickets-link'); | |
| if (ticketsLink) { | |
| ticketsLink.style.display = 'inline-block'; | |
| } | |
| } else { | |
| document.getElementById('auth-required').style.display = 'block'; | |
| document.getElementById('tickets-main').style.display = 'none'; | |
| } | |
| }) | |
| .catch(error => { | |
| console.error('Auth check failed:', error); | |
| document.getElementById('auth-required').style.display = 'block'; | |
| }); | |
| } | |
| function loadCategories() { | |
| fetch('/api/tickets/categories') | |
| .then(response => response.json()) | |
| .then(data => { | |
| if (data.success) { | |
| categories = data.categories; | |
| const categorySelect = document.getElementById('ticket-category'); | |
| categorySelect.innerHTML = '<option value="">Auto-detect</option>'; | |
| categories.forEach(category => { | |
| const option = document.createElement('option'); | |
| option.value = category.name; | |
| option.textContent = category.name; | |
| categorySelect.appendChild(option); | |
| }); | |
| } | |
| }) | |
| .catch(error => console.error('Failed to load categories:', error)); | |
| } | |
| function loadTickets() { | |
| document.getElementById('tickets-loading').style.display = 'block'; | |
| document.getElementById('tickets-empty').style.display = 'none'; | |
| document.getElementById('tickets-container').innerHTML = ''; | |
| fetch('/api/tickets/user') | |
| .then(response => response.json()) | |
| .then(data => { | |
| document.getElementById('tickets-loading').style.display = 'none'; | |
| if (data.success) { | |
| tickets = data.tickets; | |
| if (tickets.length === 0) { | |
| document.getElementById('tickets-empty').style.display = 'block'; | |
| } else { | |
| displayTickets(tickets); | |
| } | |
| } else { | |
| console.error('Failed to load tickets:', data.error); | |
| } | |
| }) | |
| .catch(error => { | |
| document.getElementById('tickets-loading').style.display = 'none'; | |
| console.error('Error loading tickets:', error); | |
| }); | |
| } | |
| function displayTickets(ticketsToShow) { | |
| const container = document.getElementById('tickets-container'); | |
| container.innerHTML = ''; | |
| ticketsToShow.forEach(ticket => { | |
| const ticketElement = createTicketElement(ticket); | |
| container.appendChild(ticketElement); | |
| }); | |
| } | |
| function createTicketElement(ticket) { | |
| const div = document.createElement('div'); | |
| div.className = 'ticket-item'; | |
| div.onclick = () => openTicketModal(ticket.ticket_number); | |
| const createdDate = new Date(ticket.created_at).toLocaleDateString(); | |
| div.innerHTML = ` | |
| <div class="ticket-header"> | |
| <div class="ticket-number">${ticket.ticket_number}</div> | |
| <div class="ticket-status status-${ticket.status}">${ticket.status.replace('_', ' ')}</div> | |
| </div> | |
| <div class="ticket-subject">${ticket.subject}</div> | |
| <div class="ticket-meta"> | |
| <div> | |
| <span class="ticket-category">${ticket.category}</span> | |
| Priority: ${ticket.priority} | |
| ${ticket.escalation_level > 0 ? `<span class="escalation-badge">Escalated L${ticket.escalation_level}</span>` : ''} | |
| </div> | |
| <div>Created: ${createdDate}</div> | |
| </div> | |
| `; | |
| return div; | |
| } | |
| // Form submission | |
| document.getElementById('create-ticket-form').addEventListener('submit', function(e) { | |
| e.preventDefault(); | |
| const formData = new FormData(e.target); | |
| const ticketData = { | |
| subject: formData.get('subject'), | |
| description: formData.get('description'), | |
| priority: formData.get('priority'), | |
| category: formData.get('category') | |
| }; | |
| const csrfToken = document.querySelector('meta[name="csrf-token"]').getAttribute('content'); | |
| fetch('/api/tickets/create', { | |
| method: 'POST', | |
| headers: { | |
| 'Content-Type': 'application/json', | |
| 'X-CSRFToken': csrfToken | |
| }, | |
| body: JSON.stringify(ticketData) | |
| }) | |
| .then(response => response.json()) | |
| .then(data => { | |
| if (data.success) { | |
| alert(`Ticket created successfully! Ticket number: ${data.ticket_number}`); | |
| e.target.reset(); | |
| loadTickets(); // Refresh the tickets list | |
| } else { | |
| alert('Failed to create ticket: ' + data.error); | |
| } | |
| }) | |
| .catch(error => { | |
| console.error('Error creating ticket:', error); | |
| alert('Error creating ticket. Please try again.'); | |
| }); | |
| }); | |
| // Status filter | |
| document.getElementById('status-filter').addEventListener('change', function(e) { | |
| const status = e.target.value; | |
| if (status === '') { | |
| displayTickets(tickets); | |
| } else { | |
| const filtered = tickets.filter(ticket => ticket.status === status); | |
| displayTickets(filtered); | |
| } | |
| }); | |
| function openTicketModal(ticketNumber) { | |
| fetch(`/api/tickets/${ticketNumber}`) | |
| .then(response => response.json()) | |
| .then(data => { | |
| if (data.success) { | |
| showTicketDetails(data.ticket, data.updates); | |
| document.getElementById('update-ticket-id').value = data.ticket.id; | |
| // Load SLA and escalation info | |
| loadSLAInfo(data.ticket.id); | |
| checkEscalationStatus(data.ticket.id); | |
| document.getElementById('ticket-modal').style.display = 'flex'; | |
| } else { | |
| alert('Failed to load ticket details: ' + data.error); | |
| } | |
| }) | |
| .catch(error => { | |
| console.error('Error loading ticket:', error); | |
| alert('Error loading ticket details.'); | |
| }); | |
| } | |
| function loadSLAInfo(ticketId) { | |
| fetch(`/api/tickets/${ticketId}/sla`) | |
| .then(response => response.json()) | |
| .then(data => { | |
| if (data.success && data.sla_metrics) { | |
| showSLAStatus(data.sla_metrics); | |
| } | |
| }) | |
| .catch(error => console.error('Error loading SLA info:', error)); | |
| } | |
| function showSLAStatus(sla) { | |
| const slaSection = document.getElementById('sla-status'); | |
| let statusClass = 'sla-met'; | |
| let statusText = 'Within SLA'; | |
| if (!sla.sla_met) { | |
| if (sla.sla_breach_hours > 0) { | |
| statusClass = 'sla-breach'; | |
| statusText = `SLA Breached (${sla.sla_breach_hours.toFixed(1)}h over)`; | |
| } else { | |
| statusClass = 'sla-warning'; | |
| statusText = 'SLA at Risk'; | |
| } | |
| } | |
| slaSection.innerHTML = ` | |
| <h4>Service Level Agreement</h4> | |
| <div class="sla-metric"> | |
| <span>Target Response Time:</span> | |
| <span>${sla.sla_target_hours}h</span> | |
| </div> | |
| <div class="sla-metric"> | |
| <span>Hours Open:</span> | |
| <span>${sla.hours_open.toFixed(1)}h</span> | |
| </div> | |
| <div class="sla-metric"> | |
| <span>Status:</span> | |
| <span class="sla-status ${statusClass}">${statusText}</span> | |
| </div> | |
| ${sla.resolution_hours ? ` | |
| <div class="sla-metric"> | |
| <span>Resolution Time:</span> | |
| <span>${sla.resolution_hours.toFixed(1)}h</span> | |
| </div>` : ''} | |
| `; | |
| slaSection.style.display = 'block'; | |
| } | |
| function checkEscalationStatus(ticketId) { | |
| fetch(`/api/tickets/${ticketId}/escalation-check`) | |
| .then(response => response.json()) | |
| .then(data => { | |
| if (data.success) { | |
| const escalationSection = document.getElementById('escalation-actions'); | |
| if (data.escalation_check.needs_escalation || data.escalation_check.status !== 'resolved') { | |
| escalationSection.style.display = 'block'; | |
| window.currentTicketId = ticketId; // Store for escalation function | |
| } | |
| } | |
| }) | |
| .catch(error => console.error('Error checking escalation:', error)); | |
| } | |
| function escalateTicket() { | |
| if (!window.currentTicketId) { | |
| alert('No ticket selected for escalation'); | |
| return; | |
| } | |
| const reason = prompt('Please provide a reason for escalation:'); | |
| if (!reason) return; | |
| fetch(`/api/tickets/${window.currentTicketId}/escalate`, { | |
| method: 'POST', | |
| headers: { | |
| 'Content-Type': 'application/json', | |
| }, | |
| body: JSON.stringify({ | |
| reason: reason | |
| }) | |
| }) | |
| .then(response => response.json()) | |
| .then(data => { | |
| if (data.success) { | |
| alert('Ticket escalated successfully! Our team will prioritize your request.'); | |
| closeTicketModal(); | |
| loadTickets(); // Refresh the tickets list | |
| } else { | |
| alert('Failed to escalate ticket: ' + data.error); | |
| } | |
| }) | |
| .catch(error => { | |
| console.error('Error escalating ticket:', error); | |
| alert('Error escalating ticket. Please try again.'); | |
| }); | |
| } | |
| function showTicketDetails(ticket, updates) { | |
| const detailsContainer = document.getElementById('ticket-details'); | |
| const createdDate = new Date(ticket.created_at).toLocaleDateString(); | |
| let updatesHtml = ''; | |
| if (updates && updates.length > 0) { | |
| updatesHtml = '<h4 style="margin-top: 2rem;">Updates:</h4>'; | |
| updates.forEach(update => { | |
| const updateDate = new Date(update.created_at).toLocaleDateString(); | |
| updatesHtml += ` | |
| <div style="background: rgba(255,255,255,0.1); padding: 1rem; margin: 0.5rem 0; border-radius: 8px;"> | |
| <div style="font-size: 0.9rem; margin-bottom: 0.5rem;"> | |
| ${updateDate} - ${update.update_type} | |
| </div> | |
| <div>${update.message}</div> | |
| </div> | |
| `; | |
| }); | |
| } | |
| detailsContainer.innerHTML = ` | |
| <div> | |
| <h4>Ticket ${ticket.ticket_number}</h4> | |
| <p><strong>Status:</strong> <span class="ticket-status status-${ticket.status}">${ticket.status.replace('_', ' ')}</span></p> | |
| <p><strong>Category:</strong> ${ticket.category}</p> | |
| <p><strong>Priority:</strong> ${ticket.priority}</p> | |
| <p><strong>Created:</strong> ${createdDate}</p> | |
| <h5 style="margin-top: 1.5rem;">Subject:</h5> | |
| <p>${ticket.subject}</p> | |
| <h5 style="margin-top: 1.5rem;">Description:</h5> | |
| <p style="white-space: pre-wrap;">${ticket.description}</p> | |
| ${updatesHtml} | |
| </div> | |
| `; | |
| document.getElementById('ticket-modal-title').textContent = `Ticket ${ticket.ticket_number}`; | |
| // Force inline styles on modal content to avoid external CSS conflicts | |
| const modalContent = document.querySelector('#ticket-modal .modal-content'); | |
| if (modalContent) { | |
| modalContent.style.backgroundColor = 'rgba(255,255,255,0.98)'; | |
| modalContent.style.color = '#1a202c'; | |
| } | |
| } | |
| function closeTicketModal() { | |
| document.getElementById('ticket-modal').style.display = 'none'; | |
| document.getElementById('update-message').value = ''; | |
| } | |
| // Update form submission | |
| document.getElementById('update-ticket-form').addEventListener('submit', function(e) { | |
| e.preventDefault(); | |
| const ticketId = document.getElementById('update-ticket-id').value; | |
| const message = document.getElementById('update-message').value; | |
| fetch(`/api/tickets/${ticketId}/update`, { | |
| method: 'POST', | |
| headers: { | |
| 'Content-Type': 'application/json', | |
| }, | |
| body: JSON.stringify({ | |
| message: message, | |
| update_type: 'note' | |
| }) | |
| }) | |
| .then(response => { | |
| console.log('Response received, status:', response.status); | |
| // Parse JSON regardless of status | |
| return response.json().then(data => ({ data, status: response.status, ok: response.ok })); | |
| }) | |
| .then(({ data, status, ok }) => { | |
| console.log('JSON parsed, data:', data); | |
| if (!ok) { | |
| const errorMsg = data.error || `Server error: ${status}`; | |
| alert('Failed to add update: ' + errorMsg); | |
| return; | |
| } | |
| if (data.success) { | |
| alert('Update added successfully!'); | |
| document.getElementById('update-message').value = ''; | |
| // Reload the ticket details | |
| const ticketNumber = document.getElementById('ticket-modal-title').textContent.replace('Ticket ', ''); | |
| openTicketModal(ticketNumber); | |
| } else { | |
| alert('Failed to add update: ' + (data.error || 'Unknown error')); | |
| } | |
| }) | |
| .catch(error => { | |
| console.error('Error adding update:', error); | |
| console.error('Error details:', error.message); | |
| alert('Error adding update: ' + error.message); | |
| }); | |
| }); | |
| // Function to show login modal (assuming it exists in base template) | |
| function showLoginModal() { | |
| // This should trigger the login modal from the base template | |
| const authBtn = document.getElementById('auth-btn'); | |
| if (authBtn && authBtn.textContent === 'Login') { | |
| authBtn.click(); | |
| } | |
| } | |
| // Close modal on outside click | |
| document.getElementById('ticket-modal').addEventListener('click', function(e) { | |
| if (e.target === this) { | |
| closeTicketModal(); | |
| } | |
| }); | |
| </script> | |
| {% endblock %} | |