From 551ced053eb30e80ace84af03c4baefef6d4c585 Mon Sep 17 00:00:00 2001 From: Harshit Kamani Date: Sun, 1 Feb 2026 22:25:12 +0530 Subject: [PATCH 1/7] utsav feedback form --- controllers/client/utsavBooking.controller.js | 147 +++++++++++++++- ...20260201035615-add-utsav-feedback-model.js | 134 +++++++++++++++ models/associations.js | 5 +- models/utsav_feedback.model.js | 158 ++++++++++++++++++ routes/client/utsavBooking.routes.js | 8 +- 5 files changed, 449 insertions(+), 3 deletions(-) create mode 100644 migrations/20260201035615-add-utsav-feedback-model.js create mode 100644 models/utsav_feedback.model.js diff --git a/controllers/client/utsavBooking.controller.js b/controllers/client/utsavBooking.controller.js index 26c1a9d5..c1fb41e2 100644 --- a/controllers/client/utsavBooking.controller.js +++ b/controllers/client/utsavBooking.controller.js @@ -5,7 +5,8 @@ import { import { UtsavBooking, UtsavDb, - UtsavPackagesDb + UtsavPackagesDb, + UtsavFeedback } from '../../models/associations.js'; import { userCancelBooking } from '../../helpers/transactions.helper.js'; import { openUtsavSeat, sendUtsavBookingUpdateEmail } from '../../helpers/utsavBooking.helper.js'; @@ -233,3 +234,147 @@ export const FetchUtsavById = async (req, res) => { return res.status(200).send({ data: utsav[0] }); }; + + +import { Op } from 'sequelize'; +// import db from '../../models/index.js'; +// import APIError from '../../utils/APIError.js'; + +// const { UtsavFeedback, UtsavBooking } = db; + +/* -------------------------------------------------------------------------- */ +/* VALIDATE FEEDBACK ACCESS */ +/* -------------------------------------------------------------------------- */ + +export const validateUtsavFeedback = async (req, res) => { + const { utsavid, cardno } = req.query; + + if (!utsavid || !cardno) { + throw new ApiError(400, 'utsavid and cardno are required'); + } + + // 1️⃣ Check booking using utsavid (booking table) + const booking = await UtsavBooking.findOne({ + where: { + utsavid, // ✅ booking table column + cardno, + status: { + [Op.in]: ['completed', 'checkedin'], + }, + }, + }); + + if (!booking) { + throw new ApiError( + 403, + 'You are not authorized to submit feedback for this utsav' + ); + } + + // 2️⃣ Check feedback using utsav_id (feedback table column) + const existing = await UtsavFeedback.findOne({ + where: { + utsav_id: utsavid, // ✅ explicit mapping + cardno, + }, + }); + + if (existing) { + throw new ApiError(409, 'Feedback already submitted'); + } + + res.json({ success: true }); +}; + +/* -------------------------------------------------------------------------- */ +/* SUBMIT FEEDBACK */ +/* -------------------------------------------------------------------------- */ + +export const submitUtsavFeedback = async (req, res) => { + const { + cardno, + utsav_id, + mumukshu_name, + accommodation_type, + room_number, + + accommodation_rating, + qr_rating, + food_rating, + program_rating, + volunteer_rating, + infrastructure_rating, + decor_rating, + internal_transport_rating, + raj_pravas_rating, + sparsh_rating, + av_rating, + + loved_most, + improvement_suggestions, + } = req.body; + + if (!cardno || !utsav_id) { + throw new APIError(400, 'cardno and utsav_id are required'); + } + + const requiredFields = [ + mumukshu_name, + accommodation_type, + room_number, + accommodation_rating, + qr_rating, + food_rating, + program_rating, + volunteer_rating, + infrastructure_rating, + decor_rating, + internal_transport_rating, + raj_pravas_rating, + sparsh_rating, + av_rating, + loved_most, + improvement_suggestions, + ]; + + if (requiredFields.some((f) => f === null || f === undefined || f === '')) { + throw new APIError(400, 'All feedback fields are required'); + } + + const existing = await UtsavFeedback.findOne({ + where: { utsav_id, cardno }, + }); + + if (existing) { + throw new APIError(409, 'Feedback already submitted'); + } + + await UtsavFeedback.create({ + cardno, + utsav_id, + mumukshu_name, + accommodation_type, + room_number, + + accommodation_rating, + qr_rating, + food_rating, + program_rating, + volunteer_rating, + infrastructure_rating, + decor_rating, + internal_transport_rating, + raj_pravas_rating, + sparsh_rating, + av_rating, + + loved_most, + improvement_suggestions, + }); + + res.json({ + success: true, + message: 'Utsav feedback submitted successfully', + }); +}; + diff --git a/migrations/20260201035615-add-utsav-feedback-model.js b/migrations/20260201035615-add-utsav-feedback-model.js new file mode 100644 index 00000000..8544b4ea --- /dev/null +++ b/migrations/20260201035615-add-utsav-feedback-model.js @@ -0,0 +1,134 @@ + +'use strict'; + +module.exports = { + async up(queryInterface, Sequelize) { + await queryInterface.createTable('utsav_feedback', { + id: { + type: Sequelize.INTEGER, + primaryKey: true, + autoIncrement: true, + allowNull: false, + }, + + cardno: { + type: Sequelize.STRING(20), + allowNull: false, + }, + + utsav_id: { + type: Sequelize.INTEGER, + allowNull: false, + }, + + mumukshu_name: { + type: Sequelize.STRING(255), + allowNull: false, + }, + + accommodation_type: { + type: Sequelize.STRING(100), + allowNull: false, + }, + + room_number: { + type: Sequelize.STRING(50), + allowNull: false, + }, + + accommodation_rating: { + type: Sequelize.TINYINT, + allowNull: false, + }, + + qr_rating: { + type: Sequelize.TINYINT, + allowNull: false, + }, + + food_rating: { + type: Sequelize.TINYINT, + allowNull: false, + }, + + program_rating: { + type: Sequelize.TINYINT, + allowNull: false, + }, + + volunteer_rating: { + type: Sequelize.TINYINT, + allowNull: false, + }, + + infrastructure_rating: { + type: Sequelize.TINYINT, + allowNull: false, + }, + + decor_rating: { + type: Sequelize.TINYINT, + allowNull: false, + }, + + internal_transport_rating: { + type: Sequelize.TINYINT, + allowNull: false, + }, + + raj_pravas_rating: { + type: Sequelize.TINYINT, + allowNull: false, + }, + + sparsh_rating: { + type: Sequelize.TINYINT, + allowNull: false, + }, + + av_rating: { + type: Sequelize.TINYINT, + allowNull: false, + }, + + loved_most: { + type: Sequelize.TEXT, + allowNull: false, + }, + + improvement_suggestions: { + type: Sequelize.TEXT, + allowNull: false, + }, + + created_at: { + type: Sequelize.DATE, + allowNull: false, + defaultValue: Sequelize.literal('CURRENT_TIMESTAMP'), + }, + + updated_at: { + type: Sequelize.DATE, + allowNull: false, + defaultValue: Sequelize.literal( + 'CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP' + ), + }, + }); + + // Prevent duplicate feedback per user per utsav + await queryInterface.addConstraint('utsav_feedback', { + fields: ['utsav_id', 'cardno'], + type: 'unique', + name: 'unique_utsav_feedback_per_user', + }); + + await queryInterface.addIndex('utsav_feedback', ['utsav_id']); + await queryInterface.addIndex('utsav_feedback', ['cardno']); + }, + + async down(queryInterface) { + await queryInterface.dropTable('utsav_feedback'); + }, +}; + diff --git a/models/associations.js b/models/associations.js index fc45dabd..f40abca8 100644 --- a/models/associations.js +++ b/models/associations.js @@ -36,6 +36,7 @@ import Updates from './updates.model.js'; import AdhyayanFeedback from './adhyayan_feedback.model.js'; import RazorpaySettlementRecon from './razorpay_settlement_recon.model.js'; import ShibirAttendanceDb from './shibir_attendance_db.model.js' +import UtsavFeedback from './utsav_feedback_model.js' // CardDb CardDb.hasMany(GateRecord, { @@ -541,5 +542,7 @@ export { Updates, AdhyayanFeedback, RazorpaySettlementRecon, - ShibirAttendanceDb + ShibirAttendanceDb, + UtsavFeedback }; + diff --git a/models/utsav_feedback.model.js b/models/utsav_feedback.model.js new file mode 100644 index 00000000..db7ef2f5 --- /dev/null +++ b/models/utsav_feedback.model.js @@ -0,0 +1,158 @@ +import { DataTypes } from 'sequelize'; +import sequelize from '../config/database.js'; +import { + RESEARCH_CENTRE, + STATUS_CLOSED, + STATUS_OPEN +} from '../config/constants.js'; +// import { UtsavFeedback } from './associations.js'; + + const UtsavFeedback = sequelize.define( + 'UtsavFeedback', + { + id: { + type: DataTypes.INTEGER, + primaryKey: true, + autoIncrement: true, + }, + + /* ------------------------------------------------------------------ */ + /* USER / CONTEXT */ + /* ------------------------------------------------------------------ */ + + cardno: { + type: DataTypes.STRING(20), + allowNull: false, + }, + + utsav_id: { + type: DataTypes.INTEGER, + allowNull: false, + }, + + mumukshu_name: { + type: DataTypes.STRING(255), + allowNull: false, + }, + + accommodation_type: { + type: DataTypes.STRING(100), + allowNull: false, + }, + + room_number: { + type: DataTypes.STRING(50), + allowNull: false, + }, + + /* ------------------------------------------------------------------ */ + /* RATINGS */ + /* ------------------------------------------------------------------ */ + + accommodation_rating: { + type: DataTypes.TINYINT, + allowNull: false, + }, + + qr_rating: { + type: DataTypes.TINYINT, + allowNull: false, + }, + + food_rating: { + type: DataTypes.TINYINT, + allowNull: false, + }, + + program_rating: { + type: DataTypes.TINYINT, + allowNull: false, + }, + + volunteer_rating: { + type: DataTypes.TINYINT, + allowNull: false, + }, + + infrastructure_rating: { + type: DataTypes.TINYINT, + allowNull: false, + }, + + decor_rating: { + type: DataTypes.TINYINT, + allowNull: false, + }, + + internal_transport_rating: { + type: DataTypes.TINYINT, + allowNull: false, + }, + + raj_pravas_rating: { + type: DataTypes.TINYINT, + allowNull: false, + }, + + sparsh_rating: { + type: DataTypes.TINYINT, + allowNull: false, + }, + + av_rating: { + type: DataTypes.TINYINT, + allowNull: false, + }, + + /* ------------------------------------------------------------------ */ + /* FEEDBACK */ + /* ------------------------------------------------------------------ */ + + loved_most: { + type: DataTypes.TEXT, + allowNull: false, + }, + + improvement_suggestions: { + type: DataTypes.TEXT, + allowNull: false, + }, + }, + { + tableName: 'utsav_feedback', + timestamps: true, + underscored: true, + + indexes: [ + { + fields: ['utsav_id'], + }, + { + fields: ['cardno'], + }, + { + unique: true, + fields: ['utsav_id', 'cardno'], // prevents duplicate feedback + }, + ], + } + ); + + /* ---------------------------------------------------------------------- */ + /* ASSOCIATIONS */ + /* ---------------------------------------------------------------------- */ + + UtsavFeedback.associate = (models) => { + // Optional – if you already have these models + UtsavFeedback.belongsTo(models.UtsavDb, { + foreignKey: 'utsav_id', + }); + + UtsavFeedback.belongsTo(models.CardDb, { + foreignKey: 'cardno', + targetKey: 'cardno', + }); + }; + +export default UtsavFeedback; + diff --git a/routes/client/utsavBooking.routes.js b/routes/client/utsavBooking.routes.js index 8a04b852..3cfef4d7 100644 --- a/routes/client/utsavBooking.routes.js +++ b/routes/client/utsavBooking.routes.js @@ -4,7 +4,9 @@ import { FetchUpcoming, ViewUtsavBookings, CancelUtsavBooking, - FetchUtsavById + FetchUtsavById, + submitUtsavFeedback, + validateUtsavFeedback } from '../../controllers/client/utsavBooking.controller.js'; import { validateCard } from '../../middleware/validate.js'; import CatchAsync from '../../utils/CatchAsync.js'; @@ -15,5 +17,9 @@ router.get('/upcoming', CatchAsync(FetchUpcoming)); router.get('/booking', CatchAsync(ViewUtsavBookings)); router.delete('/booking', CatchAsync(CancelUtsavBooking)); router.get('/:id', CatchAsync(FetchUtsavById)); +router.post('/feedback', CatchAsync(submitUtsavFeedback)); +router.get('/feedback/validate', CatchAsync(validateUtsavFeedback)); + export default router; + From ffc59b45a0f0a2a583bd813b0c1615f7534cce59 Mon Sep 17 00:00:00 2001 From: vendz Date: Tue, 3 Feb 2026 21:09:47 -0800 Subject: [PATCH 2/7] feat: Implement Utsav feedback functionality by adding a new model, migration, associations, and removing unnecessary fields. --- config/constants.js | 6 +- controllers/client/utsavBooking.controller.js | 112 ++----- helpers/utsavBooking.helper.js | 68 +++- ...20260201035615-add-utsav-feedback-model.js | 96 +++--- models/associations.js | 18 +- models/index.js | 17 +- models/utsav_feedback.model.js | 300 +++++++++--------- routes/client/utsavBooking.routes.js | 2 - 8 files changed, 315 insertions(+), 304 deletions(-) diff --git a/config/constants.js b/config/constants.js index ef2d6b84..82fecad6 100644 --- a/config/constants.js +++ b/config/constants.js @@ -64,7 +64,6 @@ export const STATUS_SEVA_KUTIR = 'SEVA KUTIR'; export const STATUS_GUEST = 'GUEST'; export const AMT_TYPE_LATE_CHECKOUT_ROOM = 'late_checkout_room'; - // ROOM export const ROOM_DETAIL = 'Room Booking'; export const ROOM_WL = 'WL'; @@ -142,6 +141,11 @@ export const ERR_TRAVEL_ALREADY_BOOKED = 'Travel already booked'; export const ERR_FLAT_ALREADY_BOOKED = 'Flat already booked for one or more mumukshus during selected dates'; export const ERR_UTSAV_ALREADY_BOOKED = 'Utsav already booked'; +export const ERR_UTSAV_NOT_FOUND = 'Utsav not found'; +export const ERR_UTSAV_FEEDBACK_NOT_ALLOWED = + 'You are not eligible to submit feedback for this utsav'; +export const ERR_UTSAV_FEEDBACK_ALREADY_SUBMITTED = + 'Feedback already submitted for this utsav'; export const ERR_FEEDBACK_ALREADY_SUBMITTED = 'Feedback already submitted for this adhyayan'; diff --git a/controllers/client/utsavBooking.controller.js b/controllers/client/utsavBooking.controller.js index c1fb41e2..99ca8c31 100644 --- a/controllers/client/utsavBooking.controller.js +++ b/controllers/client/utsavBooking.controller.js @@ -5,11 +5,14 @@ import { import { UtsavBooking, UtsavDb, - UtsavPackagesDb, UtsavFeedback } from '../../models/associations.js'; import { userCancelBooking } from '../../helpers/transactions.helper.js'; -import { openUtsavSeat, sendUtsavBookingUpdateEmail } from '../../helpers/utsavBooking.helper.js'; +import { + openUtsavSeat, + sendUtsavBookingUpdateEmail, + validateFeedbackEligibility +} from '../../helpers/utsavBooking.helper.js'; import moment from 'moment'; import database from '../../config/database.js'; import ApiError from '../../utils/ApiError.js'; @@ -163,9 +166,8 @@ export const CancelUtsavBooking = async (req, res) => { where: { id: booking.utsavid } }); await openUtsavSeat(utsav, booking.cardno, req.user.username, t); - + await t.commit(); - if (booking.bookedBy) { const other = getOtherBookingUser(booking, req.user.cardno); @@ -182,7 +184,6 @@ export const CancelUtsavBooking = async (req, res) => { }); } } - await sendUtsavBookingUpdateEmail(booking, utsav); @@ -235,69 +236,30 @@ export const FetchUtsavById = async (req, res) => { return res.status(200).send({ data: utsav[0] }); }; - -import { Op } from 'sequelize'; -// import db from '../../models/index.js'; -// import APIError from '../../utils/APIError.js'; - -// const { UtsavFeedback, UtsavBooking } = db; - -/* -------------------------------------------------------------------------- */ -/* VALIDATE FEEDBACK ACCESS */ -/* -------------------------------------------------------------------------- */ - export const validateUtsavFeedback = async (req, res) => { - const { utsavid, cardno } = req.query; + const { utsav_id } = req.query; - if (!utsavid || !cardno) { - throw new ApiError(400, 'utsavid and cardno are required'); + if (!utsav_id) { + throw new ApiError(400, 'Utsav ID is required'); } - // 1️⃣ Check booking using utsavid (booking table) - const booking = await UtsavBooking.findOne({ - where: { - utsavid, // ✅ booking table column - cardno, - status: { - [Op.in]: ['completed', 'checkedin'], - }, - }, - }); - - if (!booking) { - throw new ApiError( - 403, - 'You are not authorized to submit feedback for this utsav' - ); - } + await validateFeedbackEligibility(req.user.cardno, utsav_id); - // 2️⃣ Check feedback using utsav_id (feedback table column) - const existing = await UtsavFeedback.findOne({ - where: { - utsav_id: utsavid, // ✅ explicit mapping - cardno, - }, + return res.status(200).send({ + message: 'Feedback is allowed' }); +}; - if (existing) { - throw new ApiError(409, 'Feedback already submitted'); - } +export const submitUtsavFeedback = async (req, res) => { + const { utsav_id } = req.body; - res.json({ success: true }); -}; + if (!utsav_id) { + throw new ApiError(400, 'utsav_id is required'); + } -/* -------------------------------------------------------------------------- */ -/* SUBMIT FEEDBACK */ -/* -------------------------------------------------------------------------- */ + await validateFeedbackEligibility(req.user.cardno, utsav_id); -export const submitUtsavFeedback = async (req, res) => { const { - cardno, - utsav_id, - mumukshu_name, - accommodation_type, - room_number, - accommodation_rating, qr_rating, food_rating, @@ -309,19 +271,11 @@ export const submitUtsavFeedback = async (req, res) => { raj_pravas_rating, sparsh_rating, av_rating, - loved_most, - improvement_suggestions, + improvement_suggestions } = req.body; - if (!cardno || !utsav_id) { - throw new APIError(400, 'cardno and utsav_id are required'); - } - const requiredFields = [ - mumukshu_name, - accommodation_type, - room_number, accommodation_rating, qr_rating, food_rating, @@ -334,28 +288,16 @@ export const submitUtsavFeedback = async (req, res) => { sparsh_rating, av_rating, loved_most, - improvement_suggestions, + improvement_suggestions ]; if (requiredFields.some((f) => f === null || f === undefined || f === '')) { - throw new APIError(400, 'All feedback fields are required'); - } - - const existing = await UtsavFeedback.findOne({ - where: { utsav_id, cardno }, - }); - - if (existing) { - throw new APIError(409, 'Feedback already submitted'); + throw new ApiError(400, 'All feedback fields are required'); } await UtsavFeedback.create({ - cardno, + cardno: req.user.cardno, utsav_id, - mumukshu_name, - accommodation_type, - room_number, - accommodation_rating, qr_rating, food_rating, @@ -367,14 +309,12 @@ export const submitUtsavFeedback = async (req, res) => { raj_pravas_rating, sparsh_rating, av_rating, - loved_most, - improvement_suggestions, + improvement_suggestions }); - res.json({ + return res.status(201).json({ success: true, - message: 'Utsav feedback submitted successfully', + message: 'Utsav feedback submitted successfully' }); }; - diff --git a/helpers/utsavBooking.helper.js b/helpers/utsavBooking.helper.js index abba0152..df15e3b6 100644 --- a/helpers/utsavBooking.helper.js +++ b/helpers/utsavBooking.helper.js @@ -8,13 +8,19 @@ import { ERR_UTSAV_ALREADY_BOOKED, STATUS_AVAILABLE, STATUS_CANCELLED, - STATUS_ADMIN_CANCELLED + STATUS_ADMIN_CANCELLED, + ERR_UTSAV_NOT_FOUND, + FEEDBACK_ELIGIBILITY_HOUR, + ERR_UTSAV_FEEDBACK_NOT_ALLOWED, + STATUS_CASH_COMPLETED, + ERR_UTSAV_FEEDBACK_ALREADY_SUBMITTED } from '../config/constants.js'; import { UtsavDb, UtsavPackagesDb, UtsavBooking, - CardDb + CardDb, + UtsavFeedback } from '../models/associations.js'; import { createPendingTransaction, @@ -28,6 +34,7 @@ import { isDateRangeOverlapping, validateBlockedDates } from '../controllers/helper.js'; +import moment from 'moment'; import database from '../config/database.js'; import sendMail from '../utils/sendMail.js'; const SAMVATSARI_PACKAGE_ID = 21; @@ -533,3 +540,60 @@ export async function findUtsavOnBoundaryDates(checkin, checkout) { return utsav; } + +export async function validateFeedbackEligibility(cardno, utsav_id) { + const utsav = await UtsavDb.findOne({ + where: { id: utsav_id } + }); + + if (!utsav) { + throw new ApiError(404, ERR_UTSAV_NOT_FOUND); + } + + const now = moment().tz('Asia/Kolkata'); + const feedbackStartDate = moment(utsav.end_date) + .tz('Asia/Kolkata') + .hour(FEEDBACK_ELIGIBILITY_HOUR) + .minute(0) + .second(0); + + // Check if feedback period has started + if (now.isBefore(feedbackStartDate)) { + throw new ApiError(400, ERR_UTSAV_FEEDBACK_NOT_ALLOWED); + } + + // Check if more than 15 days have passed since adhyayan ended + const daysSinceEnd = now.diff(feedbackStartDate, 'days'); + if (daysSinceEnd > 8) { + throw new ApiError( + 400, + 'Feedback submission is only allowed within 8 days after the utsav ends' + ); + } + + // Check if user has a confirmed booking for this adhyayan + const booking = await UtsavBooking.findOne({ + where: { + cardno, + utsavid: utsav_id, + status: [STATUS_CONFIRMED, STATUS_CASH_COMPLETED] + } + }); + + if (!booking) { + throw new ApiError(403, ERR_UTSAV_FEEDBACK_NOT_ALLOWED); + } + + const existingFeedback = await UtsavFeedback.findOne({ + where: { + cardno, + utsav_id + } + }); + + if (existingFeedback) { + throw new ApiError(400, ERR_UTSAV_FEEDBACK_ALREADY_SUBMITTED); + } + + return { utsav, booking }; +} diff --git a/migrations/20260201035615-add-utsav-feedback-model.js b/migrations/20260201035615-add-utsav-feedback-model.js index 8544b4ea..960adedd 100644 --- a/migrations/20260201035615-add-utsav-feedback-model.js +++ b/migrations/20260201035615-add-utsav-feedback-model.js @@ -1,4 +1,3 @@ - 'use strict'; module.exports = { @@ -8,119 +7,115 @@ module.exports = { type: Sequelize.INTEGER, primaryKey: true, autoIncrement: true, - allowNull: false, + allowNull: false }, cardno: { - type: Sequelize.STRING(20), + type: Sequelize.STRING, allowNull: false, + references: { + model: 'card_db', + key: 'cardno' + }, + onDelete: 'CASCADE', + onUpdate: 'CASCADE' }, utsav_id: { type: Sequelize.INTEGER, allowNull: false, - }, - - mumukshu_name: { - type: Sequelize.STRING(255), - allowNull: false, - }, - - accommodation_type: { - type: Sequelize.STRING(100), - allowNull: false, - }, - - room_number: { - type: Sequelize.STRING(50), - allowNull: false, + references: { + model: 'utsav_db', + key: 'id' + }, + onDelete: 'CASCADE', + onUpdate: 'CASCADE' }, accommodation_rating: { - type: Sequelize.TINYINT, - allowNull: false, + type: Sequelize.INTEGER, + allowNull: false }, qr_rating: { - type: Sequelize.TINYINT, - allowNull: false, + type: Sequelize.INTEGER, + allowNull: false }, food_rating: { - type: Sequelize.TINYINT, - allowNull: false, + type: Sequelize.INTEGER, + allowNull: false }, program_rating: { - type: Sequelize.TINYINT, - allowNull: false, + type: Sequelize.INTEGER, + allowNull: false }, volunteer_rating: { - type: Sequelize.TINYINT, - allowNull: false, + type: Sequelize.INTEGER, + allowNull: false }, infrastructure_rating: { - type: Sequelize.TINYINT, - allowNull: false, + type: Sequelize.INTEGER, + allowNull: false }, decor_rating: { - type: Sequelize.TINYINT, - allowNull: false, + type: Sequelize.INTEGER, + allowNull: false }, internal_transport_rating: { - type: Sequelize.TINYINT, - allowNull: false, + type: Sequelize.INTEGER, + allowNull: false }, raj_pravas_rating: { - type: Sequelize.TINYINT, - allowNull: false, + type: Sequelize.INTEGER, + allowNull: false }, sparsh_rating: { - type: Sequelize.TINYINT, - allowNull: false, + type: Sequelize.INTEGER, + allowNull: false }, av_rating: { - type: Sequelize.TINYINT, - allowNull: false, + type: Sequelize.INTEGER, + allowNull: false }, loved_most: { type: Sequelize.TEXT, - allowNull: false, + allowNull: false }, improvement_suggestions: { type: Sequelize.TEXT, - allowNull: false, + allowNull: false }, - created_at: { + createdAt: { type: Sequelize.DATE, allowNull: false, - defaultValue: Sequelize.literal('CURRENT_TIMESTAMP'), + defaultValue: Sequelize.literal('CURRENT_TIMESTAMP') }, - updated_at: { + updatedAt: { type: Sequelize.DATE, allowNull: false, defaultValue: Sequelize.literal( 'CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP' - ), - }, + ) + } }); - // Prevent duplicate feedback per user per utsav await queryInterface.addConstraint('utsav_feedback', { fields: ['utsav_id', 'cardno'], type: 'unique', - name: 'unique_utsav_feedback_per_user', + name: 'unique_feedback_per_user_per_utsav' }); await queryInterface.addIndex('utsav_feedback', ['utsav_id']); @@ -129,6 +124,5 @@ module.exports = { async down(queryInterface) { await queryInterface.dropTable('utsav_feedback'); - }, + } }; - diff --git a/models/associations.js b/models/associations.js index f40abca8..8ce86fc5 100644 --- a/models/associations.js +++ b/models/associations.js @@ -35,8 +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 ShibirAttendanceDb from './shibir_attendance_db.model.js' -import UtsavFeedback from './utsav_feedback_model.js' +import ShibirAttendanceDb from './shibir_attendance_db.model.js'; +import UtsavFeedback from './utsav_feedback.model.js'; // CardDb CardDb.hasMany(GateRecord, { @@ -120,6 +120,12 @@ CardDb.hasOne(UtsavBooking, { onDelete: 'CASCADE', onUpdate: 'CASCADE' }); +CardDb.hasMany(UtsavFeedback, { + foreignKey: 'cardno', + sourceKey: 'cardno', + onDelete: 'CASCADE', + onUpdate: 'CASCADE' +}); CardDb.hasOne(UtsavBooking, { as: 'utsavBookedByCard', foreignKey: 'bookedBy', @@ -363,6 +369,12 @@ UtsavDb.hasMany(UtsavBooking, { onDelete: 'CASCADE', onUpdate: 'CASCADE' }); +UtsavDb.hasMany(UtsavFeedback, { + foreignKey: 'utsav_id', + sourceKey: 'id', + onDelete: 'CASCADE', + onUpdate: 'CASCADE' +}); UtsavBooking.belongsTo(UtsavDb, { foreignKey: 'utsavid', targetKey: 'id' @@ -503,7 +515,6 @@ ShibirAttendanceDb.belongsTo(ShibirBookingDb, { targetKey: 'bookingid' }); - export { CardDb, Transactions, @@ -545,4 +556,3 @@ export { ShibirAttendanceDb, UtsavFeedback }; - diff --git a/models/index.js b/models/index.js index 80a5308d..fa1c2870 100644 --- a/models/index.js +++ b/models/index.js @@ -7,22 +7,25 @@ import Sequelize from 'sequelize'; import sequelize from '../config/database.js'; const __filename = fileURLToPath(import.meta.url); +const __dirname = path.dirname(__filename); const basename = path.basename(__filename); const db = {}; -fs.readdirSync(path.dirname(__filename)) +const modelFiles = fs + .readdirSync(__dirname) .filter( (file) => file.indexOf('.') !== 0 && file !== basename && file.slice(-3) === '.js' && file.indexOf('.test.js') === -1 - ) - .forEach(async (file) => { - const modelModule = await import(path.join(path.dirname(__filename), file)); - const model = modelModule.default(sequelize, Sequelize.DataTypes); - db[model.name] = model; - }); + ); + +for (const file of modelFiles) { + const modelModule = await import(path.join(__dirname, file)); + const model = modelModule.default; + db[model.name] = model; +} Object.keys(db).forEach((modelName) => { if (db[modelName].associate) { diff --git a/models/utsav_feedback.model.js b/models/utsav_feedback.model.js index db7ef2f5..fd171535 100644 --- a/models/utsav_feedback.model.js +++ b/models/utsav_feedback.model.js @@ -1,158 +1,156 @@ import { DataTypes } from 'sequelize'; import sequelize from '../config/database.js'; -import { - RESEARCH_CENTRE, - STATUS_CLOSED, - STATUS_OPEN -} from '../config/constants.js'; -// import { UtsavFeedback } from './associations.js'; - const UtsavFeedback = sequelize.define( - 'UtsavFeedback', - { - id: { - type: DataTypes.INTEGER, - primaryKey: true, - autoIncrement: true, - }, - - /* ------------------------------------------------------------------ */ - /* USER / CONTEXT */ - /* ------------------------------------------------------------------ */ - - cardno: { - type: DataTypes.STRING(20), - allowNull: false, - }, - - utsav_id: { - type: DataTypes.INTEGER, - allowNull: false, - }, - - mumukshu_name: { - type: DataTypes.STRING(255), - allowNull: false, - }, - - accommodation_type: { - type: DataTypes.STRING(100), - allowNull: false, - }, - - room_number: { - type: DataTypes.STRING(50), - allowNull: false, - }, - - /* ------------------------------------------------------------------ */ - /* RATINGS */ - /* ------------------------------------------------------------------ */ - - accommodation_rating: { - type: DataTypes.TINYINT, - allowNull: false, - }, - - qr_rating: { - type: DataTypes.TINYINT, - allowNull: false, - }, - - food_rating: { - type: DataTypes.TINYINT, - allowNull: false, - }, - - program_rating: { - type: DataTypes.TINYINT, - allowNull: false, - }, - - volunteer_rating: { - type: DataTypes.TINYINT, - allowNull: false, - }, - - infrastructure_rating: { - type: DataTypes.TINYINT, - allowNull: false, - }, - - decor_rating: { - type: DataTypes.TINYINT, - allowNull: false, - }, - - internal_transport_rating: { - type: DataTypes.TINYINT, - allowNull: false, - }, - - raj_pravas_rating: { - type: DataTypes.TINYINT, - allowNull: false, - }, - - sparsh_rating: { - type: DataTypes.TINYINT, - allowNull: false, - }, - - av_rating: { - type: DataTypes.TINYINT, - allowNull: false, - }, - - /* ------------------------------------------------------------------ */ - /* FEEDBACK */ - /* ------------------------------------------------------------------ */ - - loved_most: { - type: DataTypes.TEXT, - allowNull: false, - }, - - improvement_suggestions: { - type: DataTypes.TEXT, - allowNull: false, - }, +const UtsavFeedback = sequelize.define( + 'UtsavFeedback', + { + id: { + type: DataTypes.INTEGER, + primaryKey: true, + autoIncrement: true }, - { - tableName: 'utsav_feedback', - timestamps: true, - underscored: true, - - indexes: [ - { - fields: ['utsav_id'], - }, - { - fields: ['cardno'], - }, - { - unique: true, - fields: ['utsav_id', 'cardno'], // prevents duplicate feedback - }, - ], + cardno: { + type: DataTypes.STRING, + allowNull: false, + references: { + model: 'card_db', + key: 'cardno' + } + }, + utsav_id: { + type: DataTypes.INTEGER, + allowNull: false, + references: { + model: 'utsav_db', + key: 'id' + } + }, + accommodation_rating: { + type: DataTypes.INTEGER, + allowNull: false, + validate: { + min: 1, + max: 5 + } + }, + qr_rating: { + type: DataTypes.INTEGER, + allowNull: false, + validate: { + min: 1, + max: 5 + } + }, + food_rating: { + type: DataTypes.INTEGER, + allowNull: false, + validate: { + min: 1, + max: 5 + } + }, + program_rating: { + type: DataTypes.INTEGER, + allowNull: false, + validate: { + min: 1, + max: 5 + } + }, + volunteer_rating: { + type: DataTypes.INTEGER, + allowNull: false, + validate: { + min: 1, + max: 5 + } + }, + infrastructure_rating: { + type: DataTypes.INTEGER, + allowNull: false, + validate: { + min: 1, + max: 5 + } + }, + decor_rating: { + type: DataTypes.INTEGER, + allowNull: false, + validate: { + min: 1, + max: 5 + } + }, + internal_transport_rating: { + type: DataTypes.INTEGER, + allowNull: false, + validate: { + min: 1, + max: 5 + } + }, + raj_pravas_rating: { + type: DataTypes.INTEGER, + allowNull: false, + validate: { + min: 1, + max: 5 + } + }, + sparsh_rating: { + type: DataTypes.INTEGER, + allowNull: false, + validate: { + min: 1, + max: 5 + } + }, + av_rating: { + type: DataTypes.INTEGER, + allowNull: false, + validate: { + min: 1, + max: 5 + } + }, + loved_most: { + type: DataTypes.TEXT, + allowNull: false + }, + improvement_suggestions: { + type: DataTypes.TEXT, + allowNull: false } - ); - - /* ---------------------------------------------------------------------- */ - /* ASSOCIATIONS */ - /* ---------------------------------------------------------------------- */ - - UtsavFeedback.associate = (models) => { - // Optional – if you already have these models - UtsavFeedback.belongsTo(models.UtsavDb, { - foreignKey: 'utsav_id', - }); - - UtsavFeedback.belongsTo(models.CardDb, { - foreignKey: 'cardno', - targetKey: 'cardno', - }); - }; + }, + { + tableName: 'utsav_feedback', + timestamps: true, + indexes: [ + { + fields: ['utsav_id'] + }, + { + fields: ['cardno'] + }, + { + unique: true, + fields: ['utsav_id', 'cardno'], + name: 'unique_feedback_per_user_per_utsav' + } + ] + } +); + +UtsavFeedback.associate = (models) => { + UtsavFeedback.belongsTo(models.UtsavDb, { + foreignKey: 'utsav_id', + targetKey: 'id' + }); + + UtsavFeedback.belongsTo(models.CardDb, { + foreignKey: 'cardno', + targetKey: 'cardno' + }); +}; export default UtsavFeedback; - diff --git a/routes/client/utsavBooking.routes.js b/routes/client/utsavBooking.routes.js index 3cfef4d7..547f58e7 100644 --- a/routes/client/utsavBooking.routes.js +++ b/routes/client/utsavBooking.routes.js @@ -20,6 +20,4 @@ router.get('/:id', CatchAsync(FetchUtsavById)); router.post('/feedback', CatchAsync(submitUtsavFeedback)); router.get('/feedback/validate', CatchAsync(validateUtsavFeedback)); - export default router; - From fbbc6630f26d89547fb52d52e31ef6a4638dafd3 Mon Sep 17 00:00:00 2001 From: Harshit Kamani Date: Sun, 8 Feb 2026 12:43:02 +0530 Subject: [PATCH 3/7] changed feedback start from end date to. start date --- config/constants.js | 2 +- helpers/adhyayanBooking.helper.js | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/config/constants.js b/config/constants.js index 82fecad6..889c00c0 100644 --- a/config/constants.js +++ b/config/constants.js @@ -152,7 +152,7 @@ export const ERR_FEEDBACK_ALREADY_SUBMITTED = export const ERR_FEEDBACK_NOT_ALLOWED = 'You are not eligible to submit feedback for this adhyayan'; export const ERR_ADHYAYAN_NOT_COMPLETED = - 'Cannot submit feedback for ongoing or future adhyayan'; + 'c'; export const MSG_BOOKING_SUCCESSFUL = 'Booking successful'; export const MSG_UPDATE_SUCCESSFUL = 'Update successful'; diff --git a/helpers/adhyayanBooking.helper.js b/helpers/adhyayanBooking.helper.js index f32518b8..7aafbcc8 100644 --- a/helpers/adhyayanBooking.helper.js +++ b/helpers/adhyayanBooking.helper.js @@ -368,7 +368,7 @@ export async function validateFeedbackEligibility(cardno, shibir_id) { } const now = moment().tz('Asia/Kolkata'); - const feedbackStartDate = moment(adhyayan.end_date) + const feedbackStartDate = moment(adhyayan.start_date) .tz('Asia/Kolkata') .hour(FEEDBACK_ELIGIBILITY_HOUR) .minute(0) From a664fd4f18f01f096b62c45cec119dcd5b36c154 Mon Sep 17 00:00:00 2001 From: Harshit Kamani Date: Sun, 8 Feb 2026 12:43:56 +0530 Subject: [PATCH 4/7] reverse feedback time - wrong branch --- helpers/adhyayanBooking.helper.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/helpers/adhyayanBooking.helper.js b/helpers/adhyayanBooking.helper.js index 7aafbcc8..f32518b8 100644 --- a/helpers/adhyayanBooking.helper.js +++ b/helpers/adhyayanBooking.helper.js @@ -368,7 +368,7 @@ export async function validateFeedbackEligibility(cardno, shibir_id) { } const now = moment().tz('Asia/Kolkata'); - const feedbackStartDate = moment(adhyayan.start_date) + const feedbackStartDate = moment(adhyayan.end_date) .tz('Asia/Kolkata') .hour(FEEDBACK_ELIGIBILITY_HOUR) .minute(0) From 6d3c8b402d4258940d39d9069ef05630c2ea23cd Mon Sep 17 00:00:00 2001 From: Harshit Kamani Date: Thu, 19 Feb 2026 14:34:07 +0530 Subject: [PATCH 5/7] added status checkin for feedback eligibiliity --- helpers/utsavBooking.helper.js | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/helpers/utsavBooking.helper.js b/helpers/utsavBooking.helper.js index df15e3b6..35abb134 100644 --- a/helpers/utsavBooking.helper.js +++ b/helpers/utsavBooking.helper.js @@ -13,7 +13,8 @@ import { FEEDBACK_ELIGIBILITY_HOUR, ERR_UTSAV_FEEDBACK_NOT_ALLOWED, STATUS_CASH_COMPLETED, - ERR_UTSAV_FEEDBACK_ALREADY_SUBMITTED + ERR_UTSAV_FEEDBACK_ALREADY_SUBMITTED, + ROOM_STATUS_CHECKEDIN } from '../config/constants.js'; import { UtsavDb, @@ -576,7 +577,7 @@ export async function validateFeedbackEligibility(cardno, utsav_id) { where: { cardno, utsavid: utsav_id, - status: [STATUS_CONFIRMED, STATUS_CASH_COMPLETED] + status: [STATUS_CONFIRMED, STATUS_CASH_COMPLETED, ROOM_STATUS_CHECKEDIN] } }); From 0f4db2ba6658855a9a746e451fa4da395e3309b5 Mon Sep 17 00:00:00 2001 From: Harshit Kamani Date: Fri, 20 Feb 2026 00:22:16 +0530 Subject: [PATCH 6/7] utsav feedback button --- controllers/client/utsavBooking.controller.js | 80 ++++++++++++++++++- helpers/utsavBooking.helper.js | 2 +- 2 files changed, 79 insertions(+), 3 deletions(-) diff --git a/controllers/client/utsavBooking.controller.js b/controllers/client/utsavBooking.controller.js index 99ca8c31..24aade23 100644 --- a/controllers/client/utsavBooking.controller.js +++ b/controllers/client/utsavBooking.controller.js @@ -1,6 +1,8 @@ import { ERR_BOOKING_NOT_FOUND, - MSG_CANCEL_SUCCESSFUL + MSG_CANCEL_SUCCESSFUL, + STATUS_CONFIRMED, + ROOM_STATUS_CHECKEDIN } from '../../config/constants.js'; import { UtsavBooking, @@ -13,7 +15,7 @@ import { sendUtsavBookingUpdateEmail, validateFeedbackEligibility } from '../../helpers/utsavBooking.helper.js'; -import moment from 'moment'; +import moment from 'moment-timezone'; import database from '../../config/database.js'; import ApiError from '../../utils/ApiError.js'; @@ -87,6 +89,57 @@ export const FetchUpcoming = async (req, res) => { return res.status(200).send(formattedResponse); }; +// export const ViewUtsavBookings = async (req, res) => { +// const page = parseInt(req.query.page) || 1; +// const pageSize = parseInt(req.query.page_size) || 10; +// const offset = (page - 1) * pageSize; + +// const utsavs = await database.query( +// ` +// SELECT t1.bookingid, +// t1.utsavid, +// t2.name AS utsav_name, +// t2.start_date AS utsav_start_date, +// t2.end_date AS utsav_end_date, +// t2.month, +// t2.location AS utsav_location, +// t1.packageid, +// t3.name AS package_name, +// t3.start_date AS package_start, +// t3.end_date AS package_end, +// t1.volunteer, +// t1.cardno, +// t1.bookedBy, +// t1.roomno as stay, +// t5.issuedto AS user_name, +// t1.status, +// t4.status AS transaction_status, +// t4.amount, +// t2.createdAt AS created_at +// FROM utsav_booking t1 +// LEFT JOIN utsav_db t2 ON t1.utsavid = t2.id +// LEFT JOIN utsav_packages_db t3 ON t3.id = t1.packageid +// LEFT JOIN card_db t5 ON t5.cardno = t1.cardno +// LEFT JOIN transactions t4 ON t4.bookingid = t1.bookingid +// WHERE t1.cardno = :cardno OR t1.bookedBy = :cardno +// ORDER BY created_at DESC +// LIMIT :limit +// OFFSET :offset; +// `, +// { +// replacements: { +// cardno: req.user.cardno, +// limit: pageSize, +// offset: offset +// }, +// type: database.QueryTypes.SELECT, +// raw: true +// } +// ); + +// return res.status(200).send({ data: utsavs }); +// }; + export const ViewUtsavBookings = async (req, res) => { const page = parseInt(req.query.page) || 1; const pageSize = parseInt(req.query.page_size) || 10; @@ -135,7 +188,30 @@ export const ViewUtsavBookings = async (req, res) => { } ); + // ✅ ADD THIS BLOCK (feedback eligibility) + +const now = moment().tz('Asia/Kolkata'); + +utsavs.forEach((utsav) => { + const feedbackStartDate = moment(utsav.utsav_start_date) + .tz('Asia/Kolkata') + .hour(FEEDBACK_ELIGIBILITY_HOUR) + .minute(0) + .second(0); + + const daysSinceStart = now.diff(feedbackStartDate, 'days'); + + const normalizedStatus = (utsav.status || '').toLowerCase(); + + utsav.showFeedback = + !now.isBefore(feedbackStartDate) && // after start time + daysSinceStart <= 8 && // SAME window as validator + ['confirmed', 'checkedin'].includes(normalizedStatus); +}); +console.log('DEBUG SHOW FEEDBACK:', utsavs[0]); + return res.status(200).send({ data: utsavs }); + }; export const CancelUtsavBooking = async (req, res) => { diff --git a/helpers/utsavBooking.helper.js b/helpers/utsavBooking.helper.js index 35abb134..4d2051c1 100644 --- a/helpers/utsavBooking.helper.js +++ b/helpers/utsavBooking.helper.js @@ -552,7 +552,7 @@ export async function validateFeedbackEligibility(cardno, utsav_id) { } const now = moment().tz('Asia/Kolkata'); - const feedbackStartDate = moment(utsav.end_date) + const feedbackStartDate = moment(utsav.start_date) .tz('Asia/Kolkata') .hour(FEEDBACK_ELIGIBILITY_HOUR) .minute(0) From e29d3215e6b08456821bb568d7882fb7641c588e Mon Sep 17 00:00:00 2001 From: Harshit Kamani Date: Fri, 20 Feb 2026 01:21:10 +0530 Subject: [PATCH 7/7] utsav feedback final --- controllers/client/utsavBooking.controller.js | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/controllers/client/utsavBooking.controller.js b/controllers/client/utsavBooking.controller.js index 24aade23..ff5bbad4 100644 --- a/controllers/client/utsavBooking.controller.js +++ b/controllers/client/utsavBooking.controller.js @@ -2,7 +2,8 @@ import { ERR_BOOKING_NOT_FOUND, MSG_CANCEL_SUCCESSFUL, STATUS_CONFIRMED, - ROOM_STATUS_CHECKEDIN + ROOM_STATUS_CHECKEDIN, + FEEDBACK_ELIGIBILITY_HOUR } from '../../config/constants.js'; import { UtsavBooking,