From 9fb494b441969f11b825ebd1d638dc40f9a3e4ee Mon Sep 17 00:00:00 2001 From: vvshk <122682449+hkamani111@users.noreply.github.com> Date: Sat, 3 Jan 2026 04:15:24 +0530 Subject: [PATCH] frontend for ticketing system - ignore css --- admin/ticket/tickets.html | 131 +++++++++++++++++++ admin/ticket/tickets.js | 254 ++++++++++++++++++++++++++++++++++++ package-lock.json | 267 +++++++++++++++++++++++++++++++++++++- package.json | 4 +- style/js/config.js | 4 +- 5 files changed, 656 insertions(+), 4 deletions(-) create mode 100644 admin/ticket/tickets.html create mode 100644 admin/ticket/tickets.js diff --git a/admin/ticket/tickets.html b/admin/ticket/tickets.html new file mode 100644 index 0000000..4d3ceb9 --- /dev/null +++ b/admin/ticket/tickets.html @@ -0,0 +1,131 @@ + + + + + Admin | Support Tickets + + + + + + + + + + + + + +

Support Tickets

+ + +
+ + + + + +
+ + + + + + + + + + + + + + +
TicketIssued ByServiceStatusCreatedAction
+ + +
+

+
+ +
+ +
+ + + +
+ + + +
+
+ + + + diff --git a/admin/ticket/tickets.js b/admin/ticket/tickets.js new file mode 100644 index 0000000..ca94212 --- /dev/null +++ b/admin/ticket/tickets.js @@ -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 || []); +} + +/* ===================================================== + 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 + ? ' ' + : ''; + + const tr = document.createElement('tr'); + tr.innerHTML = ` + ${dot}${t.id} + ${t.issued_by} + ${t.service} + ${t.status} + ${new Date(t.createdAt).toLocaleString()} + + + + `; + 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 = ` +

Service: ${ticket.service}

+

Issued By: ${ticket.issued_by}

+

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 = ` +

${msg.message}

+ ${msg.sender_type} • ${new Date( + msg.createdAt + ).toLocaleString()} + `; + 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; + } +} diff --git a/package-lock.json b/package-lock.json index aa8360c..37c2474 100644 --- a/package-lock.json +++ b/package-lock.json @@ -8,7 +8,9 @@ "@fortawesome/fontawesome-free": "^6.7.2", "@fortawesome/free-solid-svg-icons": "^6.7.2", "@fortawesome/react-fontawesome": "^0.2.2", - "dayjs": "^1.11.13" + "dayjs": "^1.11.13", + "socket.io": "^4.8.3", + "socket.io-client": "^4.8.3" } }, "node_modules/@fortawesome/fontawesome-common-types": { @@ -62,12 +64,139 @@ "react": ">=16.3" } }, + "node_modules/@socket.io/component-emitter": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/@socket.io/component-emitter/-/component-emitter-3.1.2.tgz", + "integrity": "sha512-9BCxFwvbGg/RsZK9tjXd8s4UcwR0MWeFQ1XEKIQVVvAGJyINdrqKMcTRyLoK8Rse1GjzLV9cwjWV1olXRWEXVA==", + "license": "MIT" + }, + "node_modules/@types/cors": { + "version": "2.8.19", + "resolved": "https://registry.npmjs.org/@types/cors/-/cors-2.8.19.tgz", + "integrity": "sha512-mFNylyeyqN93lfe/9CSxOGREz8cpzAhH+E93xJ4xWQf62V8sQ/24reV2nyzUWM6H6Xji+GGHpkbLe7pVoUEskg==", + "license": "MIT", + "dependencies": { + "@types/node": "*" + } + }, + "node_modules/@types/node": { + "version": "25.0.3", + "resolved": "https://registry.npmjs.org/@types/node/-/node-25.0.3.tgz", + "integrity": "sha512-W609buLVRVmeW693xKfzHeIV6nJGGz98uCPfeXI1ELMLXVeKYZ9m15fAMSaUPBHYLGFsVRcMmSCksQOrZV9BYA==", + "license": "MIT", + "dependencies": { + "undici-types": "~7.16.0" + } + }, + "node_modules/accepts": { + "version": "1.3.8", + "resolved": "https://registry.npmjs.org/accepts/-/accepts-1.3.8.tgz", + "integrity": "sha512-PYAthTa2m2VKxuvSD3DPC/Gy+U+sOA1LAuT8mkmRuvw+NACSaeXEQ+NHcVF7rONl6qcaxV3Uuemwawk+7+SJLw==", + "license": "MIT", + "dependencies": { + "mime-types": "~2.1.34", + "negotiator": "0.6.3" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/base64id": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/base64id/-/base64id-2.0.0.tgz", + "integrity": "sha512-lGe34o6EHj9y3Kts9R4ZYs/Gr+6N7MCaMlIFA3F1R2O5/m7K06AxfSeO5530PEERE6/WyEg3lsuyw4GHlPZHog==", + "license": "MIT", + "engines": { + "node": "^4.5.0 || >= 5.9" + } + }, + "node_modules/cookie": { + "version": "0.7.2", + "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.7.2.tgz", + "integrity": "sha512-yki5XnKuf750l50uGTllt6kKILY4nQ1eNIQatoXEByZ5dWgnKqbnqmTrBE5B4N7lrMJKQ2ytWMiTO2o0v6Ew/w==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/cors": { + "version": "2.8.5", + "resolved": "https://registry.npmjs.org/cors/-/cors-2.8.5.tgz", + "integrity": "sha512-KIHbLJqu73RGr/hnbrO9uBeixNGuvSQjul/jdFvS/KFSIH1hWVd1ng7zOHx+YrEfInLG7q4n6GHQ9cDtxv/P6g==", + "license": "MIT", + "dependencies": { + "object-assign": "^4", + "vary": "^1" + }, + "engines": { + "node": ">= 0.10" + } + }, "node_modules/dayjs": { "version": "1.11.13", "resolved": "https://registry.npmjs.org/dayjs/-/dayjs-1.11.13.tgz", "integrity": "sha512-oaMBel6gjolK862uaPQOVTA7q3TZhuSvuMQAAglQDOWYO9A91IrAOUJEyKVlqJlHE0vq5p5UXxzdPfMH/x6xNg==", "license": "MIT" }, + "node_modules/debug": { + "version": "4.4.3", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", + "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", + "license": "MIT", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/engine.io": { + "version": "6.6.5", + "resolved": "https://registry.npmjs.org/engine.io/-/engine.io-6.6.5.tgz", + "integrity": "sha512-2RZdgEbXmp5+dVbRm0P7HQUImZpICccJy7rN7Tv+SFa55pH+lxnuw6/K1ZxxBfHoYpSkHLAO92oa8O4SwFXA2A==", + "license": "MIT", + "dependencies": { + "@types/cors": "^2.8.12", + "@types/node": ">=10.0.0", + "accepts": "~1.3.4", + "base64id": "2.0.0", + "cookie": "~0.7.2", + "cors": "~2.8.5", + "debug": "~4.4.1", + "engine.io-parser": "~5.2.1", + "ws": "~8.18.3" + }, + "engines": { + "node": ">=10.2.0" + } + }, + "node_modules/engine.io-client": { + "version": "6.6.4", + "resolved": "https://registry.npmjs.org/engine.io-client/-/engine.io-client-6.6.4.tgz", + "integrity": "sha512-+kjUJnZGwzewFDw951CDWcwj35vMNf2fcj7xQWOctq1F2i1jkDdVvdFG9kM/BEChymCH36KgjnW0NsL58JYRxw==", + "license": "MIT", + "dependencies": { + "@socket.io/component-emitter": "~3.1.0", + "debug": "~4.4.1", + "engine.io-parser": "~5.2.1", + "ws": "~8.18.3", + "xmlhttprequest-ssl": "~2.1.1" + } + }, + "node_modules/engine.io-parser": { + "version": "5.2.3", + "resolved": "https://registry.npmjs.org/engine.io-parser/-/engine.io-parser-5.2.3.tgz", + "integrity": "sha512-HqD3yTBfnBxIrbnM1DoD6Pcq8NECnh8d4As1Qgh0z5Gg3jRRIqijury0CL3ghu/edArpUYiYqQiDUQBIs4np3Q==", + "license": "MIT", + "engines": { + "node": ">=10.0.0" + } + }, "node_modules/js-tokens": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", @@ -84,6 +213,42 @@ "loose-envify": "cli.js" } }, + "node_modules/mime-db": { + "version": "1.52.0", + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz", + "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/mime-types": { + "version": "2.1.35", + "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz", + "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==", + "license": "MIT", + "dependencies": { + "mime-db": "1.52.0" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "license": "MIT" + }, + "node_modules/negotiator": { + "version": "0.6.3", + "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-0.6.3.tgz", + "integrity": "sha512-+EUsqGPLsM+j/zdChZjsnX51g4XrHFOIXwfnCVPGlQk/k5giakcKsuxCObBRu6DSm9opw/O6slWbJdghQM4bBg==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, "node_modules/object-assign": { "version": "4.1.1", "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz", @@ -115,6 +280,106 @@ "version": "16.13.1", "resolved": "https://registry.npmjs.org/react-is/-/react-is-16.13.1.tgz", "integrity": "sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==" + }, + "node_modules/socket.io": { + "version": "4.8.3", + "resolved": "https://registry.npmjs.org/socket.io/-/socket.io-4.8.3.tgz", + "integrity": "sha512-2Dd78bqzzjE6KPkD5fHZmDAKRNe3J15q+YHDrIsy9WEkqttc7GY+kT9OBLSMaPbQaEd0x1BjcmtMtXkfpc+T5A==", + "license": "MIT", + "dependencies": { + "accepts": "~1.3.4", + "base64id": "~2.0.0", + "cors": "~2.8.5", + "debug": "~4.4.1", + "engine.io": "~6.6.0", + "socket.io-adapter": "~2.5.2", + "socket.io-parser": "~4.2.4" + }, + "engines": { + "node": ">=10.2.0" + } + }, + "node_modules/socket.io-adapter": { + "version": "2.5.6", + "resolved": "https://registry.npmjs.org/socket.io-adapter/-/socket.io-adapter-2.5.6.tgz", + "integrity": "sha512-DkkO/dz7MGln0dHn5bmN3pPy+JmywNICWrJqVWiVOyvXjWQFIv9c2h24JrQLLFJ2aQVQf/Cvl1vblnd4r2apLQ==", + "license": "MIT", + "dependencies": { + "debug": "~4.4.1", + "ws": "~8.18.3" + } + }, + "node_modules/socket.io-client": { + "version": "4.8.3", + "resolved": "https://registry.npmjs.org/socket.io-client/-/socket.io-client-4.8.3.tgz", + "integrity": "sha512-uP0bpjWrjQmUt5DTHq9RuoCBdFJF10cdX9X+a368j/Ft0wmaVgxlrjvK3kjvgCODOMMOz9lcaRzxmso0bTWZ/g==", + "license": "MIT", + "dependencies": { + "@socket.io/component-emitter": "~3.1.0", + "debug": "~4.4.1", + "engine.io-client": "~6.6.1", + "socket.io-parser": "~4.2.4" + }, + "engines": { + "node": ">=10.0.0" + } + }, + "node_modules/socket.io-parser": { + "version": "4.2.5", + "resolved": "https://registry.npmjs.org/socket.io-parser/-/socket.io-parser-4.2.5.tgz", + "integrity": "sha512-bPMmpy/5WWKHea5Y/jYAP6k74A+hvmRCQaJuJB6I/ML5JZq/KfNieUVo/3Mh7SAqn7TyFdIo6wqYHInG1MU1bQ==", + "license": "MIT", + "dependencies": { + "@socket.io/component-emitter": "~3.1.0", + "debug": "~4.4.1" + }, + "engines": { + "node": ">=10.0.0" + } + }, + "node_modules/undici-types": { + "version": "7.16.0", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-7.16.0.tgz", + "integrity": "sha512-Zz+aZWSj8LE6zoxD+xrjh4VfkIG8Ya6LvYkZqtUQGJPZjYl53ypCaUwWqo7eI0x66KBGeRo+mlBEkMSeSZ38Nw==", + "license": "MIT" + }, + "node_modules/vary": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/vary/-/vary-1.1.2.tgz", + "integrity": "sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/ws": { + "version": "8.18.3", + "resolved": "https://registry.npmjs.org/ws/-/ws-8.18.3.tgz", + "integrity": "sha512-PEIGCY5tSlUt50cqyMXfCzX+oOPqN0vuGqWzbcJ2xvnkzkq46oOpz7dQaTDBdfICb4N14+GARUDw2XV2N4tvzg==", + "license": "MIT", + "engines": { + "node": ">=10.0.0" + }, + "peerDependencies": { + "bufferutil": "^4.0.1", + "utf-8-validate": ">=5.0.2" + }, + "peerDependenciesMeta": { + "bufferutil": { + "optional": true + }, + "utf-8-validate": { + "optional": true + } + } + }, + "node_modules/xmlhttprequest-ssl": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/xmlhttprequest-ssl/-/xmlhttprequest-ssl-2.1.2.tgz", + "integrity": "sha512-TEU+nJVUUnA4CYJFLvK5X9AOeH4KvDvhIfm0vV1GaQRtchnG0hgK5p8hw/xjv8cunWYCsiPCSDzObPyhEwq3KQ==", + "engines": { + "node": ">=0.4.0" + } } } } diff --git a/package.json b/package.json index 936f588..a312764 100644 --- a/package.json +++ b/package.json @@ -3,6 +3,8 @@ "@fortawesome/fontawesome-free": "^6.7.2", "@fortawesome/free-solid-svg-icons": "^6.7.2", "@fortawesome/react-fontawesome": "^0.2.2", - "dayjs": "^1.11.13" + "dayjs": "^1.11.13", + "socket.io": "^4.8.3", + "socket.io-client": "^4.8.3" } } diff --git a/style/js/config.js b/style/js/config.js index 95c8c94..0c04a2b 100644 --- a/style/js/config.js +++ b/style/js/config.js @@ -1,5 +1,5 @@ -// const baseUrl = 'http://127.0.0.1:3000/api/v1'; -const baseUrl = 'https://aashray.vitraagvigyaan.org/api/v1'; +const baseUrl = 'http://127.0.0.1:3000/api/v1'; +// const baseUrl = 'https://aashray.vitraagvigyaan.org/api/v1'; // const baseUrl = 'https://aashray-backend.onrender.com/api/v1'; const CONFIG = { baseUrl,