From 4df71ce75b3e5f25fa6973ca96d10c714736de12 Mon Sep 17 00:00:00 2001 From: vendz Date: Thu, 27 Nov 2025 15:50:33 -0800 Subject: [PATCH 1/8] feat: Implement a support ticket system with client and admin interfaces, including models, controllers, routes, and tests. --- app.js | 4 + .../admin/ticketManagement.controller.js | 100 ++++++++++++++++ controllers/client/ticket.controller.js | 112 ++++++++++++++++++ models/ticket.model.js | 63 ++++++++++ models/ticket_message.model.js | 40 +++++++ routes/admin/ticketManagement.routes.js | 25 ++++ routes/client/ticket.routes.js | 20 ++++ tests/tickets.test.js | 104 ++++++++++++++++ 8 files changed, 468 insertions(+) create mode 100644 controllers/admin/ticketManagement.controller.js create mode 100644 controllers/client/ticket.controller.js create mode 100644 models/ticket.model.js create mode 100644 models/ticket_message.model.js create mode 100644 routes/admin/ticketManagement.routes.js create mode 100644 routes/client/ticket.routes.js create mode 100644 tests/tickets.test.js diff --git a/app.js b/app.js index 0a59c504..7f24ca15 100644 --- a/app.js +++ b/app.js @@ -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'; @@ -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'; @@ -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); @@ -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); diff --git a/controllers/admin/ticketManagement.controller.js b/controllers/admin/ticketManagement.controller.js new file mode 100644 index 00000000..867dd48e --- /dev/null +++ b/controllers/admin/ticketManagement.controller.js @@ -0,0 +1,100 @@ +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 + }); +}; + +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; + const { id: adminId } = req.user; // Assuming admin ID is in req.user + + if (!message) { + throw new ApiError(400, 'Message is required'); + } + + const ticket = await Ticket.findByPk(id); + if (!ticket) { + throw new ApiError(404, 'Ticket not found'); + } + + const newMessage = await TicketMessage.create({ + ticket_id: id, + sender_id: adminId, // Or admin name/email + sender_type: 'admin', + message + }); + + // Update ticket updatedBy + await ticket.update({ updatedBy: `Admin-${adminId}` }); + + res.status(201).json({ + status: 'success', + message: MSG_UPDATE_SUCCESSFUL, + data: newMessage + }); +}; + +export const updateTicketStatus = async (req, res) => { + const { id } = req.params; + const { status, admin_comments } = req.body; + const { id: adminId } = req.user; + + const ticket = await Ticket.findByPk(id); + if (!ticket) { + throw new ApiError(404, 'Ticket not found'); + } + + const updates = { updatedBy: `Admin-${adminId}` }; + if (status) updates.status = status; + if (admin_comments) updates.admin_comments = admin_comments; + + await ticket.update(updates); + + res.status(200).json({ + status: 'success', + message: MSG_UPDATE_SUCCESSFUL, + data: ticket + }); +}; diff --git a/controllers/client/ticket.controller.js b/controllers/client/ticket.controller.js new file mode 100644 index 00000000..b23fa946 --- /dev/null +++ b/controllers/client/ticket.controller.js @@ -0,0 +1,112 @@ +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_BOOKING_SUCCESSFUL, + MSG_UPDATE_SUCCESSFUL +} from '../../config/constants.js'; + +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'); + } + + const ticket = await Ticket.create({ + issued_by: cardno, + service, + description, + os, + app_version, + updatedBy: cardno + }); + + res.status(201).json({ + status: 'success', + message: MSG_BOOKING_SUCCESSFUL, + data: ticket + }); +}; + +export const getTickets = async (req, res) => { + const { cardno } = req.user; + + const tickets = await Ticket.findAll({ + where: { issued_by: cardno }, + order: [['createdAt', 'DESC']] + }); + + res.status(200).json({ + status: 'success', + message: MSG_FETCH_SUCCESSFUL, + data: tickets + }); +}; + +export const getTicketDetails = async (req, res) => { + const { id } = req.params; + const { cardno } = req.user; + + const ticket = await Ticket.findOne({ + where: { id, issued_by: cardno }, + include: [ + { + model: TicketMessage, + as: 'messages', // Note: We need to define associations + order: [['createdAt', 'ASC']] + } + ] + }); + + if (!ticket) { + throw new ApiError(404, 'Ticket not found'); + } + + // Fetch messages separately if association issue persists, but ideally association should work. + // For now, let's assume we'll add association in a separate step or here. + // Actually, let's fetch messages manually to be safe if associations aren't set up in models yet. + 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 addMessage = async (req, res) => { + const { 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, issued_by: cardno } }); + if (!ticket) { + throw new ApiError(404, 'Ticket not found'); + } + + const newMessage = await TicketMessage.create({ + ticket_id: id, + sender_id: cardno, + sender_type: 'user', + message + }); + + // Update ticket updatedBy and updatedAt + await ticket.update({ updatedBy: cardno }); + + res.status(201).json({ + status: 'success', + message: MSG_UPDATE_SUCCESSFUL, + data: newMessage + }); +}; diff --git a/models/ticket.model.js b/models/ticket.model.js new file mode 100644 index 00000000..f51fb1b9 --- /dev/null +++ b/models/ticket.model.js @@ -0,0 +1,63 @@ +import { DataTypes } from 'sequelize'; +import sequelize from '../config/database.js'; +import { + STATUS_OPEN, + STATUS_CLOSED, + STATUS_INPROGRESS +} from '../config/constants.js'; + +const Ticket = sequelize.define( + 'Ticket', + { + id: { + type: DataTypes.INTEGER, + primaryKey: true, + autoIncrement: true + }, + issued_by: { + type: DataTypes.STRING, + allowNull: false, + references: { + model: 'card_db', + key: 'cardno' + } + }, + service: { + type: DataTypes.STRING, + allowNull: false + }, + description: { + type: DataTypes.TEXT, + allowNull: false + }, + os: { + type: DataTypes.ENUM, + values: ['Android', 'iOS', 'Web', 'Other'], + allowNull: true + }, + app_version: { + type: DataTypes.STRING, + allowNull: true + }, + admin_comments: { + type: DataTypes.TEXT, + allowNull: true + }, + status: { + type: DataTypes.ENUM, + values: [STATUS_OPEN, STATUS_INPROGRESS, 'resolved', STATUS_CLOSED], + defaultValue: STATUS_OPEN, + allowNull: false + }, + updatedBy: { + type: DataTypes.STRING, + allowNull: true + } + }, + { + tableName: 'tickets', + timestamps: true + } +); + +export default Ticket; diff --git a/models/ticket_message.model.js b/models/ticket_message.model.js new file mode 100644 index 00000000..a93d9249 --- /dev/null +++ b/models/ticket_message.model.js @@ -0,0 +1,40 @@ +import { DataTypes } from 'sequelize'; +import sequelize from '../config/database.js'; + +const TicketMessage = sequelize.define( + 'TicketMessage', + { + id: { + type: DataTypes.INTEGER, + primaryKey: true, + autoIncrement: true + }, + ticket_id: { + type: DataTypes.INTEGER, + allowNull: false, + references: { + model: 'tickets', + key: 'id' + } + }, + sender_id: { + type: DataTypes.STRING, + allowNull: false + }, + sender_type: { + type: DataTypes.ENUM, + values: ['user', 'admin'], + allowNull: false + }, + message: { + type: DataTypes.TEXT, + allowNull: false + } + }, + { + tableName: 'ticket_messages', + timestamps: true + } +); + +export default TicketMessage; diff --git a/routes/admin/ticketManagement.routes.js b/routes/admin/ticketManagement.routes.js new file mode 100644 index 00000000..5b27a28e --- /dev/null +++ b/routes/admin/ticketManagement.routes.js @@ -0,0 +1,25 @@ +import express from 'express'; +import { + getAllTickets, + getTicketDetails, + adminAddMessage, + updateTicketStatus +} from '../../controllers/admin/ticketManagement.controller.js'; +import { auth, authorizeRoles } from '../../middleware/AdminAuth.js'; +import { + ROLE_SUPER_ADMIN, + ROLE_MAINTENANCE_ADMIN +} from '../../config/constants.js'; +import CatchAsync from '../../utils/CatchAsync.js'; + +const router = express.Router(); + +router.use(auth); +router.use(authorizeRoles(ROLE_SUPER_ADMIN, ROLE_MAINTENANCE_ADMIN)); + +router.get('/', CatchAsync(getAllTickets)); +router.get('/:id', CatchAsync(getTicketDetails)); +router.post('/:id/messages', CatchAsync(adminAddMessage)); +router.patch('/:id/status', CatchAsync(updateTicketStatus)); + +export default router; diff --git a/routes/client/ticket.routes.js b/routes/client/ticket.routes.js new file mode 100644 index 00000000..decfd06c --- /dev/null +++ b/routes/client/ticket.routes.js @@ -0,0 +1,20 @@ +import express from 'express'; +import { + createTicket, + getTickets, + getTicketDetails, + addMessage +} from '../../controllers/client/ticket.controller.js'; +import { validateCard } from '../../middleware/validate.js'; +import CatchAsync from '../../utils/CatchAsync.js'; + +const router = express.Router(); + +router.use(validateCard); + +router.post('/', CatchAsync(createTicket)); +router.get('/', CatchAsync(getTickets)); +router.get('/:id', CatchAsync(getTicketDetails)); +router.post('/:id/messages', CatchAsync(addMessage)); + +export default router; diff --git a/tests/tickets.test.js b/tests/tickets.test.js new file mode 100644 index 00000000..faa3346f --- /dev/null +++ b/tests/tickets.test.js @@ -0,0 +1,104 @@ +import request from 'supertest'; +import { app, sequelize } from '../app.js'; +import CardFactory from './factories/cardFactory.js'; +import Ticket from '../models/ticket.model.js'; +import TicketMessage from '../models/ticket_message.model.js'; + +describe('Ticket System', () => { + let user; + let ticketId; + + beforeAll(async () => { + // Sync database to ensure tables exist + await sequelize.sync(); + // Create a test user + user = await CardFactory.create('TEST_USER_123'); + }); + + afterAll(async () => { + // Cleanup + if (user) { + await TicketMessage.destroy({ where: {} }); + await Ticket.destroy({ where: {} }); + // We might not want to delete the user if other tests rely on it, but for this isolated test it's fine. + // However, CardFactory creates real records. + // Let's just leave it or try to clean up. + } + await sequelize.close(); + }); + + describe('POST /api/v1/tickets', () => { + it('should create a new ticket', async () => { + const response = await request(app) + .post('/api/v1/tickets') + .send({ + cardno: user.cardno, // Pass cardno for validation middleware + service: 'Plumbing', + description: 'Leaky faucet', + os: 'iOS', + app_version: '1.0.0' + }) + .expect(201); + + expect(response.body.status).toBe('success'); + expect(response.body.data.service).toBe('Plumbing'); + expect(response.body.data.issued_by).toBe(user.cardno); + + ticketId = response.body.data.id; + }); + + it('should fail without required fields', async () => { + await request(app) + .post('/api/v1/tickets') + .send({ + cardno: user.cardno + }) + .expect(400); + }); + }); + + describe('POST /api/v1/tickets/:id/messages', () => { + it('should add a message to the ticket', async () => { + const response = await request(app) + .post(`/api/v1/tickets/${ticketId}/messages`) + .send({ + cardno: user.cardno, + message: 'Is anyone looking at this?' + }) + .expect(201); + + expect(response.body.status).toBe('success'); + expect(response.body.data.message).toBe('Is anyone looking at this?'); + expect(response.body.data.sender_id).toBe(user.cardno); + }); + }); + + describe('GET /api/v1/tickets/:id', () => { + it('should get ticket details with messages', async () => { + const response = await request(app) + .get(`/api/v1/tickets/${ticketId}`) + .query({ cardno: user.cardno }) // Pass cardno in query for validation + .expect(200); + + expect(response.body.status).toBe('success'); + expect(response.body.data.id).toBe(ticketId); + expect(response.body.data.messages).toHaveLength(1); + expect(response.body.data.messages[0].message).toBe( + 'Is anyone looking at this?' + ); + }); + }); + + describe('GET /api/v1/tickets', () => { + it('should list user tickets', async () => { + const response = await request(app) + .get('/api/v1/tickets') + .query({ cardno: user.cardno }) + .expect(200); + + expect(response.body.status).toBe('success'); + expect(Array.isArray(response.body.data)).toBe(true); + expect(response.body.data.length).toBeGreaterThanOrEqual(1); + }); + }); +}); From 97bf0ba232b372ebc61dc75c73be0c6cca7ca772 Mon Sep 17 00:00:00 2001 From: vendz Date: Thu, 27 Nov 2025 17:17:15 -0800 Subject: [PATCH 2/8] fix: fixing the tickiting client system --- controllers/client/ticket.controller.js | 63 ++++++++++++------------- models/associations.js | 35 ++++++++++++++ models/ticket.model.js | 13 +---- models/ticket_message.model.js | 2 +- routes/client/ticket.routes.js | 4 +- 5 files changed, 69 insertions(+), 48 deletions(-) diff --git a/controllers/client/ticket.controller.js b/controllers/client/ticket.controller.js index b23fa946..29be82b5 100644 --- a/controllers/client/ticket.controller.js +++ b/controllers/client/ticket.controller.js @@ -3,9 +3,9 @@ import TicketMessage from '../../models/ticket_message.model.js'; import ApiError from '../../utils/ApiError.js'; import { MSG_FETCH_SUCCESSFUL, - MSG_BOOKING_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; @@ -15,19 +15,17 @@ export const createTicket = async (req, res) => { throw new ApiError(400, 'Service and description are required'); } - const ticket = await Ticket.create({ + await Ticket.create({ + id: generateTicketId(), issued_by: cardno, service, description, os, - app_version, - updatedBy: cardno + app_version }); - res.status(201).json({ - status: 'success', - message: MSG_BOOKING_SUCCESSFUL, - data: ticket + res.status(201).send({ + message: 'Successfully created ticket' }); }; @@ -39,23 +37,22 @@ export const getTickets = async (req, res) => { order: [['createdAt', 'DESC']] }); - res.status(200).json({ - status: 'success', + res.status(200).send({ message: MSG_FETCH_SUCCESSFUL, data: tickets }); }; export const getTicketDetails = async (req, res) => { - const { id } = req.params; + const { ticket_id } = req.params; const { cardno } = req.user; const ticket = await Ticket.findOne({ - where: { id, issued_by: cardno }, + where: { id: ticket_id, issued_by: cardno }, include: [ { model: TicketMessage, - as: 'messages', // Note: We need to define associations + as: 'messages', order: [['createdAt', 'ASC']] } ] @@ -65,23 +62,20 @@ export const getTicketDetails = async (req, res) => { throw new ApiError(404, 'Ticket not found'); } - // Fetch messages separately if association issue persists, but ideally association should work. - // For now, let's assume we'll add association in a separate step or here. - // Actually, let's fetch messages manually to be safe if associations aren't set up in models yet. - const messages = await TicketMessage.findAll({ - where: { ticket_id: id }, - order: [['createdAt', 'ASC']] - }); + // const messages = await TicketMessage.findAll({ + // where: { ticket_id }, + // order: [['createdAt', 'ASC']] + // }); - res.status(200).json({ - status: 'success', + res.status(200).send({ message: MSG_FETCH_SUCCESSFUL, - data: { ...ticket.toJSON(), messages } + // data: { ...ticket.toJSON(), messages } + data: ticket }); }; export const addMessage = async (req, res) => { - const { id } = req.params; + const { ticket_id } = req.params; const { message } = req.body; const { cardno } = req.user; @@ -89,24 +83,25 @@ export const addMessage = async (req, res) => { throw new ApiError(400, 'Message is required'); } - const ticket = await Ticket.findOne({ where: { id, issued_by: cardno } }); + const ticket = await Ticket.findOne({ + where: { id: ticket_id, issued_by: cardno } + }); if (!ticket) { throw new ApiError(404, 'Ticket not found'); } - const newMessage = await TicketMessage.create({ - ticket_id: id, + await TicketMessage.create({ + ticket_id, sender_id: cardno, sender_type: 'user', message }); - // Update ticket updatedBy and updatedAt - await ticket.update({ updatedBy: cardno }); - - res.status(201).json({ - status: 'success', - message: MSG_UPDATE_SUCCESSFUL, - data: newMessage + res.status(201).send({ + message: MSG_UPDATE_SUCCESSFUL }); }; + +const generateTicketId = () => { + return crypto.randomBytes(4).toString('hex').toUpperCase(); +}; diff --git a/models/associations.js b/models/associations.js index 335f33af..35c5328a 100644 --- a/models/associations.js +++ b/models/associations.js @@ -35,6 +35,8 @@ import PermanentWifiCodes from './permanent_wifi_codes.model.js'; import Updates from './updates.model.js'; import AdhyayanFeedback from './adhyayan_feedback.model.js'; import RazorpaySettlementRecon from './razorpay_settlement_recon.model.js'; +import Ticket from './ticket.model.js'; +import TicketMessage from './ticket_message.model.js'; // CardDb CardDb.hasMany(GateRecord, { @@ -174,6 +176,18 @@ CardDb.hasMany(AdhyayanFeedback, { onDelete: 'CASCADE', onUpdate: 'CASCADE' }); +CardDb.hasMany(Ticket, { + foreignKey: 'issued_by', + sourceKey: 'cardno', + onDelete: 'CASCADE', + onUpdate: 'CASCADE' +}); +CardDb.hasMany(TicketMessage, { + foreignKey: 'sender_id', + sourceKey: 'cardno', + onDelete: 'CASCADE', + onUpdate: 'CASCADE' +}); // Transactions Transactions.belongsTo(CardDb, { @@ -462,6 +476,27 @@ SupportTickets.belongsTo(CardDb, { targetKey: 'cardno' }); +// Ticket +Ticket.belongsTo(CardDb, { + foreignKey: 'issued_by', + targetKey: 'cardno' +}); +Ticket.hasMany(TicketMessage, { + as: 'messages', + foreignKey: 'ticket_id', + sourceKey: 'id', + onDelete: 'CASCADE', + onUpdate: 'CASCADE' +}); +TicketMessage.belongsTo(Ticket, { + foreignKey: 'ticket_id', + targetKey: 'id' +}); +TicketMessage.belongsTo(CardDb, { + foreignKey: 'sender_id', + targetKey: 'cardno' +}); + export { CardDb, Transactions, diff --git a/models/ticket.model.js b/models/ticket.model.js index f51fb1b9..021d987a 100644 --- a/models/ticket.model.js +++ b/models/ticket.model.js @@ -10,9 +10,8 @@ const Ticket = sequelize.define( 'Ticket', { id: { - type: DataTypes.INTEGER, - primaryKey: true, - autoIncrement: true + type: DataTypes.STRING, + primaryKey: true }, issued_by: { type: DataTypes.STRING, @@ -39,19 +38,11 @@ const Ticket = sequelize.define( type: DataTypes.STRING, allowNull: true }, - admin_comments: { - type: DataTypes.TEXT, - allowNull: true - }, status: { type: DataTypes.ENUM, values: [STATUS_OPEN, STATUS_INPROGRESS, 'resolved', STATUS_CLOSED], defaultValue: STATUS_OPEN, allowNull: false - }, - updatedBy: { - type: DataTypes.STRING, - allowNull: true } }, { diff --git a/models/ticket_message.model.js b/models/ticket_message.model.js index a93d9249..4fddfdad 100644 --- a/models/ticket_message.model.js +++ b/models/ticket_message.model.js @@ -10,7 +10,7 @@ const TicketMessage = sequelize.define( autoIncrement: true }, ticket_id: { - type: DataTypes.INTEGER, + type: DataTypes.STRING, allowNull: false, references: { model: 'tickets', diff --git a/routes/client/ticket.routes.js b/routes/client/ticket.routes.js index decfd06c..7fd79f41 100644 --- a/routes/client/ticket.routes.js +++ b/routes/client/ticket.routes.js @@ -14,7 +14,7 @@ router.use(validateCard); router.post('/', CatchAsync(createTicket)); router.get('/', CatchAsync(getTickets)); -router.get('/:id', CatchAsync(getTicketDetails)); -router.post('/:id/messages', CatchAsync(addMessage)); +router.get('/:ticket_id', CatchAsync(getTicketDetails)); +router.post('/:ticket_id/messages', CatchAsync(addMessage)); export default router; From 2e83438449a5f4cc46dfa81c8fad74629ddb491c Mon Sep 17 00:00:00 2001 From: vendz Date: Thu, 27 Nov 2025 18:26:41 -0800 Subject: [PATCH 3/8] feat: Implement pagination for fetching tickets. --- controllers/client/ticket.controller.js | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/controllers/client/ticket.controller.js b/controllers/client/ticket.controller.js index 29be82b5..30acb321 100644 --- a/controllers/client/ticket.controller.js +++ b/controllers/client/ticket.controller.js @@ -31,10 +31,15 @@ export const createTicket = async (req, res) => { 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']] + order: [['createdAt', 'DESC']], + offset, + limit: pageSize }); res.status(200).send({ From 5fdf0fe13cbafe610f63c1939e96bdf0a7039222 Mon Sep 17 00:00:00 2001 From: vendz Date: Wed, 3 Dec 2025 21:18:39 -0800 Subject: [PATCH 4/8] feat: Add endpoint and controller to allow clients to resolve their tickets. --- controllers/client/ticket.controller.js | 26 +++++++++++++++++++++++++ routes/client/ticket.routes.js | 4 +++- 2 files changed, 29 insertions(+), 1 deletion(-) diff --git a/controllers/client/ticket.controller.js b/controllers/client/ticket.controller.js index 30acb321..8de0bf8d 100644 --- a/controllers/client/ticket.controller.js +++ b/controllers/client/ticket.controller.js @@ -107,6 +107,32 @@ export const addMessage = async (req, res) => { }); }; +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 + }); + + res.status(200).send({ + message: MSG_UPDATE_SUCCESSFUL + }); +}; + const generateTicketId = () => { return crypto.randomBytes(4).toString('hex').toUpperCase(); }; diff --git a/routes/client/ticket.routes.js b/routes/client/ticket.routes.js index 7fd79f41..60e9df0e 100644 --- a/routes/client/ticket.routes.js +++ b/routes/client/ticket.routes.js @@ -3,7 +3,8 @@ import { createTicket, getTickets, getTicketDetails, - addMessage + addMessage, + resolveTicket } from '../../controllers/client/ticket.controller.js'; import { validateCard } from '../../middleware/validate.js'; import CatchAsync from '../../utils/CatchAsync.js'; @@ -16,5 +17,6 @@ router.post('/', CatchAsync(createTicket)); router.get('/', CatchAsync(getTickets)); router.get('/:ticket_id', CatchAsync(getTicketDetails)); router.post('/:ticket_id/messages', CatchAsync(addMessage)); +router.patch('/:ticket_id/resolve', CatchAsync(resolveTicket)); export default router; From 24d9a7f43510dd73f6f9d321599b789c533f8a07 Mon Sep 17 00:00:00 2001 From: vendz Date: Thu, 4 Dec 2025 12:46:03 -0800 Subject: [PATCH 5/8] feat: Prevent replies to closed tickets and automatically update ticket status to 'in progress' upon new messages. --- .../admin/ticketManagement.controller.js | 13 +++++++++++-- controllers/client/ticket.controller.js | 18 ++++++++++++------ 2 files changed, 23 insertions(+), 8 deletions(-) diff --git a/controllers/admin/ticketManagement.controller.js b/controllers/admin/ticketManagement.controller.js index 867dd48e..9910b1ec 100644 --- a/controllers/admin/ticketManagement.controller.js +++ b/controllers/admin/ticketManagement.controller.js @@ -59,6 +59,10 @@ export const adminAddMessage = async (req, res) => { throw new ApiError(404, 'Ticket not found'); } + if (ticket.status === 'closed') { + throw new ApiError(400, 'Cannot reply to a closed ticket'); + } + const newMessage = await TicketMessage.create({ ticket_id: id, sender_id: adminId, // Or admin name/email @@ -66,8 +70,13 @@ export const adminAddMessage = async (req, res) => { message }); - // Update ticket updatedBy - await ticket.update({ updatedBy: `Admin-${adminId}` }); + // Update ticket updatedBy and status if needed + const updates = { updatedBy: `Admin-${adminId}` }; + if (ticket.status === 'open') { + updates.status = 'in progress'; + } + + await ticket.update(updates); res.status(201).json({ status: 'success', diff --git a/controllers/client/ticket.controller.js b/controllers/client/ticket.controller.js index 8de0bf8d..7b82b68b 100644 --- a/controllers/client/ticket.controller.js +++ b/controllers/client/ticket.controller.js @@ -67,14 +67,8 @@ export const getTicketDetails = async (req, res) => { throw new ApiError(404, 'Ticket not found'); } - // const messages = await TicketMessage.findAll({ - // where: { ticket_id }, - // order: [['createdAt', 'ASC']] - // }); - res.status(200).send({ message: MSG_FETCH_SUCCESSFUL, - // data: { ...ticket.toJSON(), messages } data: ticket }); }; @@ -95,6 +89,10 @@ export const addMessage = async (req, res) => { 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, @@ -102,6 +100,14 @@ export const addMessage = async (req, res) => { 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'; + } + + await ticket.update(updates); + res.status(201).send({ message: MSG_UPDATE_SUCCESSFUL }); From 8cf106781f9c7d7a6015a652a6496bfe7bf61a34 Mon Sep 17 00:00:00 2001 From: vendz Date: Thu, 1 Jan 2026 16:45:04 -0800 Subject: [PATCH 6/8] feat: Remove `CardDb` association from `TicketMessage`, update ticket management to use `req.user.username` for sender and updater, and reorder admin status check. --- controllers/admin/auth.controller.js | 21 +++++++++++-------- .../admin/ticketManagement.controller.js | 16 +++++--------- models/associations.js | 10 --------- 3 files changed, 17 insertions(+), 30 deletions(-) diff --git a/controllers/admin/auth.controller.js b/controllers/admin/auth.controller.js index 67ce5064..3bb20c5b 100644 --- a/controllers/admin/auth.controller.js +++ b/controllers/admin/auth.controller.js @@ -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 } @@ -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' }); } diff --git a/controllers/admin/ticketManagement.controller.js b/controllers/admin/ticketManagement.controller.js index 9910b1ec..f89c392e 100644 --- a/controllers/admin/ticketManagement.controller.js +++ b/controllers/admin/ticketManagement.controller.js @@ -48,7 +48,6 @@ export const getTicketDetails = async (req, res) => { export const adminAddMessage = async (req, res) => { const { id } = req.params; const { message } = req.body; - const { id: adminId } = req.user; // Assuming admin ID is in req.user if (!message) { throw new ApiError(400, 'Message is required'); @@ -60,18 +59,18 @@ export const adminAddMessage = async (req, res) => { } if (ticket.status === 'closed') { - throw new ApiError(400, 'Cannot reply to a closed ticket'); + throw new ApiError(400, 'ticket is closed'); } const newMessage = await TicketMessage.create({ ticket_id: id, - sender_id: adminId, // Or admin name/email + sender_id: req.user.username, sender_type: 'admin', message }); // Update ticket updatedBy and status if needed - const updates = { updatedBy: `Admin-${adminId}` }; + const updates = { updatedBy: req.user.username }; if (ticket.status === 'open') { updates.status = 'in progress'; } @@ -87,19 +86,14 @@ export const adminAddMessage = async (req, res) => { export const updateTicketStatus = async (req, res) => { const { id } = req.params; - const { status, admin_comments } = req.body; - const { id: adminId } = req.user; + const { status } = req.body; const ticket = await Ticket.findByPk(id); if (!ticket) { throw new ApiError(404, 'Ticket not found'); } - const updates = { updatedBy: `Admin-${adminId}` }; - if (status) updates.status = status; - if (admin_comments) updates.admin_comments = admin_comments; - - await ticket.update(updates); + await ticket.update({ status, updatedBy: req.user.username }); res.status(200).json({ status: 'success', diff --git a/models/associations.js b/models/associations.js index 35c5328a..a48bab09 100644 --- a/models/associations.js +++ b/models/associations.js @@ -182,12 +182,6 @@ CardDb.hasMany(Ticket, { onDelete: 'CASCADE', onUpdate: 'CASCADE' }); -CardDb.hasMany(TicketMessage, { - foreignKey: 'sender_id', - sourceKey: 'cardno', - onDelete: 'CASCADE', - onUpdate: 'CASCADE' -}); // Transactions Transactions.belongsTo(CardDb, { @@ -492,10 +486,6 @@ TicketMessage.belongsTo(Ticket, { foreignKey: 'ticket_id', targetKey: 'id' }); -TicketMessage.belongsTo(CardDb, { - foreignKey: 'sender_id', - targetKey: 'cardno' -}); export { CardDb, From a7821528bbe3825942e221caf50bc867421b66fc Mon Sep 17 00:00:00 2001 From: vvshk <122682449+hkamani111@users.noreply.github.com> Date: Sat, 3 Jan 2026 04:15:37 +0530 Subject: [PATCH 7/8] frontend for ticketing system - ignore css --- app.js | 17 +- config/socket.js | 33 +++ .../admin/ticketManagement.controller.js | 34 ++- package-lock.json | 217 ++++++++++++++++++ package.json | 1 + 5 files changed, 296 insertions(+), 6 deletions(-) create mode 100644 config/socket.js diff --git a/app.js b/app.js index 7f24ca15..abf99bad 100644 --- a/app.js +++ b/app.js @@ -54,6 +54,10 @@ import ticketManagementRoutes from './routes/admin/ticketManagement.routes.js'; // Unified Route Imports import unifiedBookingRoutes from './routes/client/unifiedBooking.routes.js'; +import http from 'http'; +import { initSocket } from './config/socket.js'; +const app = express(); + // Ensure logs directory exists const logsDir = path.join(process.cwd(), 'logs'); if (!fs.existsSync(logsDir)) { @@ -100,7 +104,7 @@ const corsOptions = { optionSuccessStatus: 200 }; -const app = express(); + app.use(urlencoded({ extended: true })); app.use(json()); app.use(cors(corsOptions)); @@ -207,9 +211,14 @@ app.use(ErrorHandler); if (process.env.NODE_ENV != 'test') { const port = process.env.PORT || 3000; - const server = app.listen(port, () => { - logger.info(`Server is listening on port ${port}...`); - }); + const server = http.createServer(app); + +// 🔌 attach socket.io to the SAME server +initSocket(server); + +server.listen(port, () => { + logger.info(`Server is listening on port ${port}...`); +}); // Graceful shutdown handling const gracefulShutdown = async (signal) => { diff --git a/config/socket.js b/config/socket.js new file mode 100644 index 00000000..7d30a8cd --- /dev/null +++ b/config/socket.js @@ -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; +}; diff --git a/controllers/admin/ticketManagement.controller.js b/controllers/admin/ticketManagement.controller.js index f89c392e..7e25a15e 100644 --- a/controllers/admin/ticketManagement.controller.js +++ b/controllers/admin/ticketManagement.controller.js @@ -6,21 +6,51 @@ import { 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', - message: MSG_FETCH_SUCCESSFUL, data: tickets }); }; diff --git a/package-lock.json b/package-lock.json index 8b38b3a2..0d985181 100644 --- a/package-lock.json +++ b/package-lock.json @@ -29,6 +29,7 @@ "nodemailer-express-handlebars": "^6.1.2", "razorpay": "^2.9.5", "sequelize": "^6.37.7", + "socket.io": "^4.8.3", "uuid": "^9.0.1", "winston": "^3.17.0", "winston-daily-rotate-file": "^5.0.0", @@ -4201,6 +4202,12 @@ "node": ">=18.0.0" } }, + "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/babel__core": { "version": "7.20.5", "resolved": "https://registry.npmjs.org/@types/babel__core/-/babel__core-7.20.5.tgz", @@ -4246,6 +4253,15 @@ "@babel/types": "^7.20.7" } }, + "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/debug": { "version": "4.1.12", "resolved": "https://registry.npmjs.org/@types/debug/-/debug-4.1.12.tgz", @@ -5098,6 +5114,15 @@ ], "license": "MIT" }, + "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/bcrypt": { "version": "5.1.1", "resolved": "https://registry.npmjs.org/bcrypt/-/bcrypt-5.1.1.tgz", @@ -6161,6 +6186,67 @@ "node": ">= 0.8" } }, + "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-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/engine.io/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/engine.io/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/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/err-code": { "version": "2.0.3", "resolved": "https://registry.npmjs.org/err-code/-/err-code-2.0.3.tgz", @@ -10340,6 +10426,116 @@ "node": ">=8" } }, + "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-adapter/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/socket.io-adapter/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/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/socket.io-parser/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/socket.io-parser/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/socket.io/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/socket.io/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/source-map": { "version": "0.6.1", "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", @@ -11303,6 +11499,27 @@ "node": "^12.13.0 || ^14.15.0 || >=16.0.0" } }, + "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/xlsx": { "version": "0.18.5", "resolved": "https://registry.npmjs.org/xlsx/-/xlsx-0.18.5.tgz", diff --git a/package.json b/package.json index 965e6508..f23e5583 100644 --- a/package.json +++ b/package.json @@ -36,6 +36,7 @@ "nodemailer-express-handlebars": "^6.1.2", "razorpay": "^2.9.5", "sequelize": "^6.37.7", + "socket.io": "^4.8.3", "uuid": "^9.0.1", "winston": "^3.17.0", "winston-daily-rotate-file": "^5.0.0", From 13bf344a11c022c98780e00995d0ac4665c90d15 Mon Sep 17 00:00:00 2001 From: vendz Date: Sat, 3 Jan 2026 11:22:11 -0800 Subject: [PATCH 8/8] fix: remove socket code from app and add ticket models to association --- app.js | 17 ++++------------- models/associations.js | 2 ++ 2 files changed, 6 insertions(+), 13 deletions(-) diff --git a/app.js b/app.js index abf99bad..7f24ca15 100644 --- a/app.js +++ b/app.js @@ -54,10 +54,6 @@ import ticketManagementRoutes from './routes/admin/ticketManagement.routes.js'; // Unified Route Imports import unifiedBookingRoutes from './routes/client/unifiedBooking.routes.js'; -import http from 'http'; -import { initSocket } from './config/socket.js'; -const app = express(); - // Ensure logs directory exists const logsDir = path.join(process.cwd(), 'logs'); if (!fs.existsSync(logsDir)) { @@ -104,7 +100,7 @@ const corsOptions = { optionSuccessStatus: 200 }; - +const app = express(); app.use(urlencoded({ extended: true })); app.use(json()); app.use(cors(corsOptions)); @@ -211,14 +207,9 @@ app.use(ErrorHandler); if (process.env.NODE_ENV != 'test') { const port = process.env.PORT || 3000; - const server = http.createServer(app); - -// 🔌 attach socket.io to the SAME server -initSocket(server); - -server.listen(port, () => { - logger.info(`Server is listening on port ${port}...`); -}); + const server = app.listen(port, () => { + logger.info(`Server is listening on port ${port}...`); + }); // Graceful shutdown handling const gracefulShutdown = async (signal) => { diff --git a/models/associations.js b/models/associations.js index a48bab09..fafed4b7 100644 --- a/models/associations.js +++ b/models/associations.js @@ -521,6 +521,8 @@ export { RazorpayWebhook, RazorpaySettlement, SupportTickets, + Ticket, + TicketMessage, BlockDates, Updates, AdhyayanFeedback,