-
Notifications
You must be signed in to change notification settings - Fork 0
frontend for ticketing system - ignore css #103
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: dev
Are you sure you want to change the base?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change | ||||
|---|---|---|---|---|---|---|
| @@ -0,0 +1,131 @@ | ||||||
| <!DOCTYPE html> | ||||||
| <html lang="en"> | ||||||
| <head> | ||||||
| <meta charset="UTF-8" /> | ||||||
| <title>Admin | Support Tickets</title> | ||||||
|
|
||||||
| <!-- Role check --> | ||||||
| <script src="/style/js/roleCheck.js"></script> | ||||||
| <script> | ||||||
| checkRoleAccess(['superAdmin', 'maintenanceAdmin']); | ||||||
| </script> | ||||||
|
|
||||||
| <script src="https://ajax.googleapis.com/ajax/libs/jquery/1.11.2/jquery.min.js"></script> | ||||||
| <script src="../../style/js/config.js"></script> | ||||||
| <link rel="stylesheet" href="../../style/css/styles.css" /> | ||||||
|
|
||||||
| <style> | ||||||
| .drawer { | ||||||
| position: fixed; | ||||||
| top: 0; | ||||||
| right: -40%; | ||||||
| width: 40%; | ||||||
| height: 100%; | ||||||
| background: #fff; | ||||||
| box-shadow: -2px 0 8px rgba(0, 0, 0, 0.3); | ||||||
| transition: right 0.3s ease; | ||||||
| z-index: 999; | ||||||
| padding: 20px; | ||||||
| overflow-y: auto; | ||||||
| } | ||||||
|
|
||||||
| .drawer.open { | ||||||
| right: 0; | ||||||
| } | ||||||
|
|
||||||
| .messages { | ||||||
| border: 1px solid #ddd; | ||||||
| padding: 10px; | ||||||
| max-height: 300px; | ||||||
| overflow-y: auto; | ||||||
| margin-bottom: 10px; | ||||||
| } | ||||||
|
|
||||||
| .message { | ||||||
| margin-bottom: 8px; | ||||||
| } | ||||||
|
|
||||||
| .message.admin { | ||||||
| text-align: right; | ||||||
| color: #0066cc; | ||||||
| } | ||||||
|
|
||||||
| .message.user { | ||||||
| text-align: left; | ||||||
| color: #333; | ||||||
| } | ||||||
|
|
||||||
| .message span { | ||||||
| font-size: 11px; | ||||||
| color: #777; | ||||||
| } | ||||||
|
|
||||||
| textarea { | ||||||
| width: 100%; | ||||||
| height: 70px; | ||||||
| } | ||||||
| </style> | ||||||
| </head> | ||||||
|
|
||||||
| <body> | ||||||
| <h2>Support Tickets</h2> | ||||||
|
|
||||||
| <!-- Filters --> | ||||||
| <div style="margin-bottom: 10px"> | ||||||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. |
||||||
| <select id="statusFilter"> | ||||||
| <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> | ||||||
|
|
||||||
| <select id="serviceFilter"> | ||||||
| <option value="">All Services</option> | ||||||
| <option value="wifi">WiFi</option> | ||||||
| <option value="room">Room</option> | ||||||
| <option value="maintenance">Maintenance</option> | ||||||
| </select> | ||||||
|
|
||||||
| <button onclick="fetchTickets()">Apply</button> | ||||||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Using For example, you could give this button an ID and attach the listener in HTML: <button id="apply-filters-btn">Apply</button>JavaScript ( document.getElementById('apply-filters-btn').addEventListener('click', fetchTickets);This should be applied to all buttons with |
||||||
| </div> | ||||||
|
|
||||||
| <!-- Tickets Table --> | ||||||
| <table border="1" width="100%" id="ticketTable"> | ||||||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. The
Suggested change
|
||||||
| <thead> | ||||||
| <tr> | ||||||
| <th>Ticket</th> | ||||||
| <th>Issued By</th> | ||||||
| <th>Service</th> | ||||||
| <th>Status</th> | ||||||
| <th>Created</th> | ||||||
| <th>Action</th> | ||||||
| </tr> | ||||||
| </thead> | ||||||
| <tbody></tbody> | ||||||
| </table> | ||||||
|
|
||||||
| <!-- Side Drawer --> | ||||||
| <div class="drawer" id="ticketDrawer"> | ||||||
| <h3 id="ticketTitle"></h3> | ||||||
| <div id="ticketMeta"></div> | ||||||
|
|
||||||
| <hr /> | ||||||
|
|
||||||
| <div class="messages" id="messageContainer"></div> | ||||||
|
|
||||||
| <textarea | ||||||
| id="adminMessage" | ||||||
| placeholder="Type your reply..." | ||||||
| ></textarea> | ||||||
|
|
||||||
| <div style="margin-top: 10px; text-align: right"> | ||||||
| <button onclick="sendReply()">Send</button> | ||||||
| <button onclick="closeTicket()">Close Ticket</button> | ||||||
| <button onclick="closeDrawer()">Cancel</button> | ||||||
| </div> | ||||||
| </div> | ||||||
|
|
||||||
| <script src="tickets.js"></script> | ||||||
| </body> | ||||||
| </html> | ||||||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,254 @@ | ||
| let ticketListInterval = null; | ||
| let currentTicketId = null; | ||
| let refreshInterval = null; | ||
|
|
||
| document.addEventListener('DOMContentLoaded', () => { | ||
| fetchTickets(); | ||
| startTicketListRefresh(); | ||
| }); | ||
|
|
||
| /* ===================================================== | ||
| UNREAD TRACKING (LAST MESSAGE BASED) | ||
| ===================================================== */ | ||
|
|
||
| function getLastSeen(ticketId) { | ||
| return sessionStorage.getItem(`ticket_last_msg_seen_${ticketId}`); | ||
| } | ||
|
|
||
| function setLastSeen(ticketId, time) { | ||
| sessionStorage.setItem(`ticket_last_msg_seen_${ticketId}`, time); | ||
| } | ||
|
|
||
| /* ===================================================== | ||
| FETCH TICKETS (LIST VIEW) | ||
| ===================================================== */ | ||
|
|
||
| async function fetchTickets() { | ||
| const status = document.getElementById('statusFilter').value; | ||
| const service = document.getElementById('serviceFilter').value; | ||
|
|
||
| const params = new URLSearchParams(); | ||
| if (status) params.append('status', status); | ||
| if (service) params.append('service', service); | ||
|
|
||
| const res = await fetch( | ||
| `${CONFIG.basePath}/tickets?${params.toString()}`, | ||
| { | ||
| headers: { | ||
| Authorization: `Bearer ${sessionStorage.getItem('token')}` | ||
| } | ||
| } | ||
| ); | ||
|
|
||
| const result = await res.json(); | ||
| renderTicketTable(result.data || []); | ||
|
Comment on lines
+34
to
+44
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. The This should be applied to all try {
const res = await fetch(
`${CONFIG.basePath}/tickets?${params.toString()}`,
{
headers: {
Authorization: `Bearer ${sessionStorage.getItem('token')}`
}
}
);
if (!res.ok) {
console.error('Failed to fetch tickets:', res.status, res.statusText);
// Consider showing an error message to the user
return;
}
const result = await res.json();
renderTicketTable(result.data || []);
} catch (error) {
console.error('Error fetching tickets:', error);
// Consider showing an error message to the user
} |
||
| } | ||
|
|
||
| /* ===================================================== | ||
| RENDER TICKET TABLE (UNREAD INDICATOR) | ||
| ===================================================== */ | ||
|
|
||
| function renderTicketTable(tickets) { | ||
| const tbody = document.querySelector('#ticketTable tbody'); | ||
| tbody.innerHTML = ''; | ||
|
|
||
| tickets.forEach(t => { | ||
| const lastSeen = getLastSeen(t.id); | ||
| const lastMsg = t.last_message_at; | ||
|
|
||
| const unread = | ||
| lastMsg && (!lastSeen || new Date(lastMsg) > new Date(lastSeen)); | ||
|
|
||
| const dot = unread | ||
| ? '<span style="color:red;font-weight:bold;">●</span> ' | ||
| : ''; | ||
|
|
||
| const tr = document.createElement('tr'); | ||
| tr.innerHTML = ` | ||
| <td>${dot}${t.id}</td> | ||
| <td>${t.issued_by}</td> | ||
| <td>${t.service}</td> | ||
| <td>${t.status}</td> | ||
| <td>${new Date(t.createdAt).toLocaleString()}</td> | ||
| <td> | ||
| <button onclick="openTicket('${t.id}')">View</button> | ||
| </td> | ||
| `; | ||
|
Comment on lines
+67
to
+76
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Directly setting Example of a safer approach: const tr = document.createElement('tr');
const createCell = (text) => {
const td = document.createElement('td');
td.textContent = text;
return td;
};
const idCell = document.createElement('td');
idCell.innerHTML = dot; // 'dot' is safe as it's generated in the code
idCell.appendChild(document.createTextNode(t.id));
const actionCell = document.createElement('td');
const viewButton = document.createElement('button');
viewButton.textContent = 'View';
viewButton.addEventListener('click', () => openTicket(t.id));
actionCell.appendChild(viewButton);
tr.append(
idCell,
createCell(t.issued_by),
createCell(t.service),
createCell(t.status),
createCell(new Date(t.createdAt).toLocaleString()),
actionCell
);
tbody.appendChild(tr); |
||
| tbody.appendChild(tr); | ||
| }); | ||
| } | ||
|
|
||
| /* ===================================================== | ||
| OPEN TICKET (DRAWER) | ||
| ===================================================== */ | ||
|
|
||
| async function openTicket(ticketId) { | ||
| currentTicketId = ticketId; | ||
| stopTicketListRefresh(); // 👈 pause list polling | ||
| await loadTicketDetails(); | ||
| openDrawer(); | ||
| startAutoRefresh(); | ||
| } | ||
|
|
||
| /* ===================================================== | ||
| LOAD TICKET DETAILS (MESSAGES) | ||
| ===================================================== */ | ||
|
|
||
| async function loadTicketDetails() { | ||
| if (!currentTicketId) return; | ||
|
|
||
| const res = await fetch( | ||
| `${CONFIG.basePath}/tickets/${currentTicketId}`, | ||
| { | ||
| headers: { | ||
| Authorization: `Bearer ${sessionStorage.getItem('token')}` | ||
| } | ||
| } | ||
| ); | ||
|
|
||
| const result = await res.json(); | ||
| const ticket = result.data; | ||
|
|
||
| populateDrawer(ticket); | ||
|
|
||
| /* | ||
| 🔑 CRITICAL FIX: | ||
| Mark ticket as read using LAST MESSAGE TIME, | ||
| NOT current time | ||
| */ | ||
| if (ticket.messages && ticket.messages.length > 0) { | ||
| const lastMsg = | ||
| ticket.messages[ticket.messages.length - 1]; | ||
| setLastSeen(currentTicketId, lastMsg.createdAt); | ||
| } | ||
| } | ||
|
|
||
| /* ===================================================== | ||
| POPULATE DRAWER UI | ||
| ===================================================== */ | ||
|
|
||
| function populateDrawer(ticket) { | ||
| document.getElementById( | ||
| 'ticketTitle' | ||
| ).innerText = `Ticket #${ticket.id} (${ticket.status})`; | ||
|
|
||
| document.getElementById('ticketMeta').innerHTML = ` | ||
| <p><b>Service:</b> ${ticket.service}</p> | ||
| <p><b>Issued By:</b> ${ticket.issued_by}</p> | ||
| <p><b>Description:</b> ${ticket.description}</p> | ||
| `; | ||
|
Comment on lines
+135
to
+139
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Using const meta = document.getElementById('ticketMeta');
meta.innerHTML = ''; // Clear previous content
const createMetaLine = (label, value) => {
const p = document.createElement('p');
const b = document.createElement('b');
b.textContent = `${label}: `;
p.appendChild(b);
p.appendChild(document.createTextNode(value));
return p;
};
meta.appendChild(createMetaLine('Service', ticket.service));
meta.appendChild(createMetaLine('Issued By', ticket.issued_by));
meta.appendChild(createMetaLine('Description', ticket.description)); |
||
|
|
||
| const container = document.getElementById('messageContainer'); | ||
|
|
||
| const wasAtBottom = | ||
| container.scrollTop + container.clientHeight >= | ||
| container.scrollHeight - 10; | ||
|
|
||
| container.innerHTML = ''; | ||
|
|
||
| ticket.messages.forEach(msg => { | ||
| const div = document.createElement('div'); | ||
| div.className = `message ${msg.sender_type}`; | ||
| div.innerHTML = ` | ||
| <p>${msg.message}</p> | ||
| <span>${msg.sender_type} • ${new Date( | ||
| msg.createdAt | ||
| ).toLocaleString()}</span> | ||
| `; | ||
|
Comment on lines
+152
to
+157
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Setting <p></p>
<span>${msg.sender_type} • ${new Date(
msg.createdAt
).toLocaleString()}</span>
`;
div.querySelector('p').textContent = msg.message; |
||
| container.appendChild(div); | ||
| }); | ||
|
|
||
| if (wasAtBottom) { | ||
| container.scrollTop = container.scrollHeight; | ||
| } | ||
| } | ||
|
|
||
| /* ===================================================== | ||
| SEND ADMIN REPLY | ||
| ===================================================== */ | ||
|
|
||
| async function sendReply() { | ||
| const message = document.getElementById('adminMessage').value.trim(); | ||
| if (!message || !currentTicketId) return; | ||
|
|
||
| await fetch( | ||
| `${CONFIG.basePath}/tickets/${currentTicketId}/messages`, | ||
| { | ||
| method: 'POST', | ||
| headers: { | ||
| 'Content-Type': 'application/json', | ||
| Authorization: `Bearer ${sessionStorage.getItem('token')}` | ||
| }, | ||
| body: JSON.stringify({ message }) | ||
| } | ||
| ); | ||
|
|
||
| document.getElementById('adminMessage').value = ''; | ||
| await loadTicketDetails(); | ||
| } | ||
|
|
||
| /* ===================================================== | ||
| CLOSE TICKET | ||
| ===================================================== */ | ||
|
|
||
| async function closeTicket() { | ||
| if (!currentTicketId) return; | ||
|
|
||
| await fetch( | ||
| `${CONFIG.basePath}/tickets/${currentTicketId}/status`, | ||
| { | ||
| method: 'PATCH', | ||
| headers: { | ||
| 'Content-Type': 'application/json', | ||
| Authorization: `Bearer ${sessionStorage.getItem('token')}` | ||
| }, | ||
| body: JSON.stringify({ status: 'closed' }) | ||
| } | ||
| ); | ||
|
|
||
| closeDrawer(); | ||
| fetchTickets(); | ||
| } | ||
|
|
||
| /* ===================================================== | ||
| DRAWER CONTROL | ||
| ===================================================== */ | ||
|
|
||
| function openDrawer() { | ||
| document.getElementById('ticketDrawer').classList.add('open'); | ||
| } | ||
|
|
||
| function closeDrawer() { | ||
| document.getElementById('ticketDrawer').classList.remove('open'); | ||
| stopAutoRefresh(); | ||
| startTicketListRefresh(); // 👈 resume list polling | ||
| currentTicketId = null; | ||
| } | ||
|
|
||
| /* ===================================================== | ||
| AUTO REFRESH (POLLING) | ||
| ===================================================== */ | ||
|
|
||
| function startAutoRefresh() { | ||
| stopAutoRefresh(); | ||
| refreshInterval = setInterval(loadTicketDetails, 60000); | ||
| } | ||
|
|
||
| function stopAutoRefresh() { | ||
| if (refreshInterval) { | ||
| clearInterval(refreshInterval); | ||
| refreshInterval = null; | ||
| } | ||
| } | ||
|
|
||
| function startTicketListRefresh() { | ||
| stopTicketListRefresh(); | ||
| ticketListInterval = setInterval(fetchTickets, 5000); | ||
| } | ||
|
|
||
| function stopTicketListRefresh() { | ||
| if (ticketListInterval) { | ||
| clearInterval(ticketListInterval); | ||
| ticketListInterval = null; | ||
| } | ||
| } | ||
|
Comment on lines
+232
to
+254
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. The application uses polling via |
||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The version of jQuery being used (1.11.2) is very old and has known security vulnerabilities, including Cross-Site Scripting (XSS). It is critical to update to a modern, secure version of jQuery (e.g., 3.x series) to protect the application and its users.