Skip to content
4 changes: 4 additions & 0 deletions app.js
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@ import mumukshuRoutes from './routes/client/mumukshuBooking.routes.js';
import paymentRoutes from './routes/client/payment.routes.js';
import supportRoutes from './routes/client/support.routes.js';
import updateRoutes from './routes/client/updates.routes.js';
import ticketRoutes from './routes/client/ticket.routes.js';

// Admin Route Imports
import authRoutes from './routes/admin/auth.routes.js';
Expand All @@ -48,6 +49,7 @@ import {
} from './routes/admin/utsavManagement.routes.js';
import avtManagementRoutes from './routes/admin/avtManagement.routes.js';
import wifiManagementRoutes from './routes/admin/wifiManagement.routes.js';
import ticketManagementRoutes from './routes/admin/ticketManagement.routes.js';

// Unified Route Imports
import unifiedBookingRoutes from './routes/client/unifiedBooking.routes.js';
Expand Down Expand Up @@ -170,6 +172,7 @@ app.use('/api/v1/profile', profileRoutes);
app.use('/api/v1/location', locationRoutes);
app.use('/api/v1/razorpay', paymentRoutes);
app.use('/api/v1/support', supportRoutes);
app.use('/api/v1/tickets', ticketRoutes);

// Admin Routes
app.use('/api/v1/admin/sudo', adminControlRoutes);
Expand All @@ -188,6 +191,7 @@ app.use('/api/v1/admin/utsav', utsavPublicRouter); // No auth
app.use('/api/v1/admin/utsav', utsavAdminRouter); // With auth
app.use('/api/v1/admin/avt', avtManagementRoutes);
app.use('/api/v1/admin/wifi', wifiManagementRoutes);
app.use('/api/v1/admin/tickets', ticketManagementRoutes);

// Unified Routes
app.use('/api/v1/unified', unifiedBookingRoutes);
Expand Down
33 changes: 33 additions & 0 deletions config/socket.js
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;
};
21 changes: 12 additions & 9 deletions controllers/admin/auth.controller.js
Original file line number Diff line number Diff line change
Expand Up @@ -10,13 +10,14 @@ export const login = async (req, res) => {
const admin = await AdminUsers.findOne({
where: { username: username }
});
if (admin.dataValues.status === STATUS_INACTIVE)
throw new ApiError(401, 'Account Deactivated');

if (!admin) {
throw new ApiError(404, 'Invalid Username');
}


if (admin.dataValues.status === STATUS_INACTIVE)
throw new ApiError(401, 'Account Deactivated');

const roles = await AdminRoles.findAll({
attributes: ['role_name'],
where: { user_id: admin.dataValues.id, status: STATUS_ACTIVE }
Expand Down Expand Up @@ -80,21 +81,23 @@ export const createAdmin = async (req, res) => {
return res.status(201).send({ message: 'successfully created admin' });
};



export const resetPassword = async (req, res) => {
const { username, newPassword } = req.body;

if (!username || !newPassword) {
return res.status(400).json({ message: 'Username and new password are required' });
if (!username || !newPassword) {
return res
.status(400)
.json({ message: 'Username and new password are required' });
}

if (newPassword.length < 8) {
return res.status(400).json({ message: 'Password must be at least 8 characters long' });
return res
.status(400)
.json({ message: 'Password must be at least 8 characters long' });
}

try {
const user = await AdminUsers.findOne({ where: { username } });
const user = await AdminUsers.findOne({ where: { username } });
if (!user) {
return res.status(404).json({ message: 'User not found' });
}
Expand Down
133 changes: 133 additions & 0 deletions controllers/admin/ticketManagement.controller.js
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 }
});
Comment on lines +61 to +75
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

high

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 Ticket and TicketMessage models.

Once the association is defined (e.g., Ticket.hasMany(TicketMessage, { as: 'messages', ... })), you can use include to fetch everything at once. This simplifies the code, removes the need for manual data merging, and improves performance.

Suggested change
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 }
});
const ticket = await Ticket.findByPk(id, {
include: [
{
model: TicketMessage,
as: 'messages', // This alias must match the one in the association definition
},
],
order: [[{ model: TicketMessage, as: 'messages' }, 'createdAt', 'ASC']],
});
if (!ticket) {
throw new ApiError(404, 'Ticket not found');
}
res.status(200).json({
status: 'success',
message: MSG_FETCH_SUCCESSFUL,
data: ticket,
});

};

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');
Copy link

Copilot AI Jan 2, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The error message starts with a lowercase letter 'ticket is closed', which is inconsistent with other error messages in the same file (line 53: 'Message is required', line 58: 'Ticket not found'). Error messages should be consistently capitalized for better readability and professionalism.

Suggested change
throw new ApiError(400, 'ticket is closed');
throw new ApiError(400, 'Ticket is closed');

Copilot uses AI. Check for mistakes.
}

const newMessage = await TicketMessage.create({
ticket_id: id,
sender_id: req.user.username,
sender_type: 'admin',
message
});

// Update ticket updatedBy and status if needed
const updates = { updatedBy: req.user.username };
if (ticket.status === 'open') {
updates.status = 'in progress';
}
Comment on lines +91 to +106
Copy link

Copilot AI Jan 2, 2026

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.

Copilot uses AI. Check for mistakes.

await ticket.update(updates);

res.status(201).json({
status: 'success',
message: MSG_UPDATE_SUCCESSFUL,
data: newMessage
});
};

export const updateTicketStatus = async (req, res) => {
const { id } = req.params;
const { status } = req.body;

const ticket = await Ticket.findByPk(id);
if (!ticket) {
throw new ApiError(404, 'Ticket not found');
}

await ticket.update({ status, updatedBy: req.user.username });

res.status(200).json({
status: 'success',
message: MSG_UPDATE_SUCCESSFUL,
data: ticket
});
};
144 changes: 144 additions & 0 deletions controllers/client/ticket.controller.js
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
Copy link

Copilot AI Jan 2, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The createTicket response doesn't return the created ticket data, but the test expects it (lines 44-47 check response.body.data.service, response.body.data.issued_by, and response.body.data.id). The response should include the created ticket in the data field. Consider storing the result of Ticket.create() and including it in the response.

Copilot uses AI. Check for mistakes.
};

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
Copy link

Copilot AI Jan 2, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The response format is inconsistent with the client controller responses. While this admin controller correctly includes 'status', 'message', and 'data' fields, the client ticket controller (ticket.controller.js) only includes 'message' and 'data'. For consistent API design, both client and admin endpoints should use the same response structure. The admin format with the 'status' field is the better choice as it matches what the tests expect.

Copilot uses AI. Check for mistakes.
};

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
Copy link

Copilot AI Jan 2, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The order clause is incorrectly placed inside the include object. In Sequelize, when ordering included associations, the order should be specified at the top-level findOne options or using a nested array format like: order: [[{model: TicketMessage, as: 'messages'}, 'createdAt', 'ASC']]. The current placement inside the include object will be ignored by Sequelize, and messages will not be ordered as intended.

Suggested change
as: 'messages',
order: [['createdAt', 'ASC']]
}
]
as: 'messages'
}
],
order: [[{ model: TicketMessage, as: 'messages' }, 'createdAt', 'ASC']]

Copilot uses AI. Check for mistakes.
});

if (!ticket) {
throw new ApiError(404, 'Ticket not found');
}

res.status(200).send({
message: MSG_FETCH_SUCCESSFUL,
data: ticket
});
};

export const addMessage = async (req, res) => {
const { ticket_id } = req.params;
const { message } = req.body;
const { cardno } = req.user;

if (!message) {
throw new ApiError(400, 'Message is required');
}

const ticket = await Ticket.findOne({
where: { id: ticket_id, issued_by: cardno }
});
if (!ticket) {
throw new ApiError(404, 'Ticket not found');
}

if (ticket.status === 'closed') {
throw new ApiError(400, 'Cannot reply to a closed ticket');
}

await TicketMessage.create({
ticket_id,
sender_id: cardno,
sender_type: 'user',
message
});

// If ticket was resolved, move back to in progress since user is replying
const updates = { updatedBy: cardno };
if (ticket.status === 'resolved') {
updates.status = 'in progress';
}
Comment on lines +92 to +107
Copy link

Copilot AI Jan 2, 2026

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 uses AI. Check for mistakes.

await ticket.update(updates);

res.status(201).send({
message: MSG_UPDATE_SUCCESSFUL
});
};

export const resolveTicket = async (req, res) => {
const { ticket_id } = req.params;
const { cardno } = req.user;

const ticket = await Ticket.findOne({
where: { id: ticket_id, issued_by: cardno }
});

if (!ticket) {
throw new ApiError(404, 'Ticket not found');
}

if (ticket.status === 'closed') {
throw new ApiError(400, 'Ticket is already closed');
}

await ticket.update({
status: 'closed',
updatedBy: cardno
Comment on lines +128 to +134
Copy link

Copilot AI Jan 2, 2026

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.

Copilot uses AI. Check for mistakes.
});

res.status(200).send({
message: MSG_UPDATE_SUCCESSFUL
});
};

const generateTicketId = () => {
return crypto.randomBytes(4).toString('hex').toUpperCase();
};
Loading