-
Notifications
You must be signed in to change notification settings - Fork 0
feat: Implement a support ticket system with client and admin interfa… #227
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: main
Are you sure you want to change the base?
Changes from all commits
4df71ce
97bf0ba
2e83438
5fdf0fe
24d9a7f
8cf1067
a782152
13bf344
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,33 @@ | ||
| import { Server } from 'socket.io'; | ||
|
|
||
| let io; | ||
|
|
||
| export const initSocket = (httpServer) => { | ||
| io = new Server(httpServer, { | ||
| cors: { | ||
| origin: '*', // restrict later if needed | ||
| methods: ['GET', 'POST'] | ||
| } | ||
| }); | ||
|
|
||
| io.on('connection', socket => { | ||
| console.log('Admin connected:', socket.id); | ||
|
|
||
| socket.on('join_admin', () => { | ||
| socket.join('admins'); | ||
| }); | ||
|
|
||
| socket.on('disconnect', () => { | ||
| console.log('Disconnected:', socket.id); | ||
| }); | ||
| }); | ||
|
|
||
| return io; | ||
| }; | ||
|
|
||
| export const getIO = () => { | ||
| if (!io) { | ||
| throw new Error('Socket.io not initialized'); | ||
| } | ||
| return io; | ||
| }; |
| Original file line number | Diff line number | Diff line change | ||||
|---|---|---|---|---|---|---|
| @@ -0,0 +1,133 @@ | ||||||
| import Ticket from '../../models/ticket.model.js'; | ||||||
| import TicketMessage from '../../models/ticket_message.model.js'; | ||||||
| import ApiError from '../../utils/ApiError.js'; | ||||||
| import { | ||||||
| MSG_FETCH_SUCCESSFUL, | ||||||
| MSG_UPDATE_SUCCESSFUL | ||||||
| } from '../../config/constants.js'; | ||||||
|
|
||||||
| // export const getAllTickets = async (req, res) => { | ||||||
| // const { status, service } = req.query; | ||||||
| // const where = {}; | ||||||
|
|
||||||
| // if (status) where.status = status; | ||||||
| // if (service) where.service = service; | ||||||
|
|
||||||
| // const tickets = await Ticket.findAll({ | ||||||
| // where, | ||||||
| // order: [['createdAt', 'DESC']] | ||||||
| // }); | ||||||
|
|
||||||
| // res.status(200).json({ | ||||||
| // status: 'success', | ||||||
| // message: MSG_FETCH_SUCCESSFUL, | ||||||
| // data: tickets | ||||||
| // }); | ||||||
| // }; | ||||||
| import { Sequelize } from 'sequelize'; | ||||||
|
|
||||||
| export const getAllTickets = async (req, res) => { | ||||||
| const { status, service } = req.query; | ||||||
| const where = {}; | ||||||
| if (status) where.status = status; | ||||||
| if (service) where.service = service; | ||||||
|
|
||||||
| const tickets = await Ticket.findAll({ | ||||||
| where, | ||||||
| attributes: { | ||||||
| include: [ | ||||||
| [ | ||||||
| Sequelize.literal(`( | ||||||
| SELECT MAX(createdAt) | ||||||
| FROM ticket_messages | ||||||
| WHERE ticket_messages.ticket_id = Ticket.id | ||||||
| )`), | ||||||
| 'last_message_at' | ||||||
| ] | ||||||
| ] | ||||||
| }, | ||||||
| order: [['createdAt', 'DESC']] | ||||||
| }); | ||||||
|
|
||||||
| res.status(200).json({ | ||||||
| status: 'success', | ||||||
| data: tickets | ||||||
| }); | ||||||
| }; | ||||||
|
|
||||||
| export const getTicketDetails = async (req, res) => { | ||||||
| const { id } = req.params; | ||||||
|
|
||||||
| const ticket = await Ticket.findByPk(id); | ||||||
| if (!ticket) { | ||||||
| throw new ApiError(404, 'Ticket not found'); | ||||||
| } | ||||||
|
|
||||||
| const messages = await TicketMessage.findAll({ | ||||||
| where: { ticket_id: id }, | ||||||
| order: [['createdAt', 'ASC']] | ||||||
| }); | ||||||
|
|
||||||
| res.status(200).json({ | ||||||
| status: 'success', | ||||||
| message: MSG_FETCH_SUCCESSFUL, | ||||||
| data: { ...ticket.toJSON(), messages } | ||||||
| }); | ||||||
| }; | ||||||
|
|
||||||
| export const adminAddMessage = async (req, res) => { | ||||||
| const { id } = req.params; | ||||||
| const { message } = req.body; | ||||||
|
|
||||||
| if (!message) { | ||||||
| throw new ApiError(400, 'Message is required'); | ||||||
| } | ||||||
|
|
||||||
| const ticket = await Ticket.findByPk(id); | ||||||
| if (!ticket) { | ||||||
| throw new ApiError(404, 'Ticket not found'); | ||||||
| } | ||||||
|
|
||||||
| if (ticket.status === 'closed') { | ||||||
| throw new ApiError(400, 'ticket is closed'); | ||||||
|
||||||
| throw new ApiError(400, 'ticket is closed'); | |
| throw new ApiError(400, 'Ticket is closed'); |
Copilot
AI
Jan 2, 2026
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.
Hard-coded status strings 'closed', 'open', and 'in progress' should use constants STATUS_CLOSED, STATUS_OPEN, and STATUS_INPROGRESS from config/constants.js. This is inconsistent with codebase conventions as seen in other maintenance controllers.
| Original file line number | Diff line number | Diff line change | ||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| @@ -0,0 +1,144 @@ | ||||||||||||||||||
| import Ticket from '../../models/ticket.model.js'; | ||||||||||||||||||
| import TicketMessage from '../../models/ticket_message.model.js'; | ||||||||||||||||||
| import ApiError from '../../utils/ApiError.js'; | ||||||||||||||||||
| import { | ||||||||||||||||||
| MSG_FETCH_SUCCESSFUL, | ||||||||||||||||||
| MSG_UPDATE_SUCCESSFUL | ||||||||||||||||||
| } from '../../config/constants.js'; | ||||||||||||||||||
| import crypto from 'crypto'; | ||||||||||||||||||
|
|
||||||||||||||||||
| export const createTicket = async (req, res) => { | ||||||||||||||||||
| const { service, description, os, app_version } = req.body; | ||||||||||||||||||
| const { cardno } = req.user; | ||||||||||||||||||
|
|
||||||||||||||||||
| if (!service || !description) { | ||||||||||||||||||
| throw new ApiError(400, 'Service and description are required'); | ||||||||||||||||||
| } | ||||||||||||||||||
|
|
||||||||||||||||||
| await Ticket.create({ | ||||||||||||||||||
| id: generateTicketId(), | ||||||||||||||||||
| issued_by: cardno, | ||||||||||||||||||
| service, | ||||||||||||||||||
| description, | ||||||||||||||||||
| os, | ||||||||||||||||||
| app_version | ||||||||||||||||||
| }); | ||||||||||||||||||
|
|
||||||||||||||||||
| res.status(201).send({ | ||||||||||||||||||
| message: 'Successfully created ticket' | ||||||||||||||||||
| }); | ||||||||||||||||||
|
Comment on lines
+18
to
+29
|
||||||||||||||||||
| }; | ||||||||||||||||||
|
|
||||||||||||||||||
| export const getTickets = async (req, res) => { | ||||||||||||||||||
| const { cardno } = req.user; | ||||||||||||||||||
| const page = parseInt(req.query.page) || 1; | ||||||||||||||||||
| const pageSize = parseInt(req.query.page_size) || 10; | ||||||||||||||||||
| const offset = (page - 1) * pageSize; | ||||||||||||||||||
|
|
||||||||||||||||||
| const tickets = await Ticket.findAll({ | ||||||||||||||||||
| where: { issued_by: cardno }, | ||||||||||||||||||
| order: [['createdAt', 'DESC']], | ||||||||||||||||||
| offset, | ||||||||||||||||||
| limit: pageSize | ||||||||||||||||||
| }); | ||||||||||||||||||
|
|
||||||||||||||||||
| res.status(200).send({ | ||||||||||||||||||
| message: MSG_FETCH_SUCCESSFUL, | ||||||||||||||||||
| data: tickets | ||||||||||||||||||
| }); | ||||||||||||||||||
|
Comment on lines
+27
to
+48
|
||||||||||||||||||
| }; | ||||||||||||||||||
|
|
||||||||||||||||||
| export const getTicketDetails = async (req, res) => { | ||||||||||||||||||
| const { ticket_id } = req.params; | ||||||||||||||||||
| const { cardno } = req.user; | ||||||||||||||||||
|
|
||||||||||||||||||
| const ticket = await Ticket.findOne({ | ||||||||||||||||||
| where: { id: ticket_id, issued_by: cardno }, | ||||||||||||||||||
| include: [ | ||||||||||||||||||
| { | ||||||||||||||||||
| model: TicketMessage, | ||||||||||||||||||
| as: 'messages', | ||||||||||||||||||
| order: [['createdAt', 'ASC']] | ||||||||||||||||||
| } | ||||||||||||||||||
| ] | ||||||||||||||||||
|
Comment on lines
+60
to
+63
|
||||||||||||||||||
| as: 'messages', | |
| order: [['createdAt', 'ASC']] | |
| } | |
| ] | |
| as: 'messages' | |
| } | |
| ], | |
| order: [[{ model: TicketMessage, as: 'messages' }, 'createdAt', 'ASC']] |
Copilot
AI
Jan 2, 2026
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.
Hard-coded status strings 'closed' and 'in progress' are used instead of the constants STATUS_CLOSED and STATUS_INPROGRESS from config/constants.js. This is inconsistent with the codebase convention as seen in other controllers like admin/maintenanceManagement.controller.js which consistently uses STATUS_OPEN, STATUS_CLOSED, and STATUS_INPROGRESS constants. Using hard-coded strings makes the code more error-prone and harder to maintain.
Copilot
AI
Jan 2, 2026
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.
Hard-coded status strings 'closed' and 'resolved' are used instead of constants. The model defines 'resolved' as a valid status, but there's no corresponding STATUS_RESOLVED constant in config/constants.js. Either add STATUS_RESOLVED to constants.js or remove 'resolved' as a status option. Additionally, 'closed' should use STATUS_CLOSED constant for consistency.
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.
This function is inefficient as it performs two separate database queries to fetch a ticket and its associated messages. This can be optimized into a single query by properly defining the Sequelize associations between
TicketandTicketMessagemodels.Once the association is defined (e.g.,
Ticket.hasMany(TicketMessage, { as: 'messages', ... })), you can useincludeto fetch everything at once. This simplifies the code, removes the need for manual data merging, and improves performance.