chatbot / templates /tickets.html
Moderator404's picture
Upload 38 files
a5778f2 verified
Raw
History Blame Contribute Delete
28.1 kB
{% 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()">&times;</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 !important;
}
.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) !important;
color: #1a202c !important;
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 !important;
}
/* 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 !important;
}
.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 %}