diff --git a/config/constants.js b/config/constants.js index ef2d6b84..889c00c0 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,13 +141,18 @@ 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'; 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/controllers/client/utsavBooking.controller.js b/controllers/client/utsavBooking.controller.js index 26c1a9d5..ff5bbad4 100644 --- a/controllers/client/utsavBooking.controller.js +++ b/controllers/client/utsavBooking.controller.js @@ -1,15 +1,22 @@ import { ERR_BOOKING_NOT_FOUND, - MSG_CANCEL_SUCCESSFUL + MSG_CANCEL_SUCCESSFUL, + STATUS_CONFIRMED, + ROOM_STATUS_CHECKEDIN, + FEEDBACK_ELIGIBILITY_HOUR } from '../../config/constants.js'; 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 moment from 'moment'; +import { + openUtsavSeat, + sendUtsavBookingUpdateEmail, + validateFeedbackEligibility +} from '../../helpers/utsavBooking.helper.js'; +import moment from 'moment-timezone'; import database from '../../config/database.js'; import ApiError from '../../utils/ApiError.js'; @@ -83,6 +90,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; @@ -131,7 +189,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) => { @@ -162,9 +243,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); @@ -181,7 +261,6 @@ export const CancelUtsavBooking = async (req, res) => { }); } } - await sendUtsavBookingUpdateEmail(booking, utsav); @@ -233,3 +312,86 @@ export const FetchUtsavById = async (req, res) => { return res.status(200).send({ data: utsav[0] }); }; + +export const validateUtsavFeedback = async (req, res) => { + const { utsav_id } = req.query; + + if (!utsav_id) { + throw new ApiError(400, 'Utsav ID is required'); + } + + await validateFeedbackEligibility(req.user.cardno, utsav_id); + + return res.status(200).send({ + message: 'Feedback is allowed' + }); +}; + +export const submitUtsavFeedback = async (req, res) => { + const { utsav_id } = req.body; + + if (!utsav_id) { + throw new ApiError(400, 'utsav_id is required'); + } + + await validateFeedbackEligibility(req.user.cardno, utsav_id); + + const { + 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; + + const requiredFields = [ + 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'); + } + + await UtsavFeedback.create({ + cardno: req.user.cardno, + utsav_id, + 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 + }); + + return res.status(201).json({ + success: true, + message: 'Utsav feedback submitted successfully' + }); +}; diff --git a/helpers/utsavBooking.helper.js b/helpers/utsavBooking.helper.js index abba0152..4d2051c1 100644 --- a/helpers/utsavBooking.helper.js +++ b/helpers/utsavBooking.helper.js @@ -8,13 +8,20 @@ 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, + ROOM_STATUS_CHECKEDIN } from '../config/constants.js'; import { UtsavDb, UtsavPackagesDb, UtsavBooking, - CardDb + CardDb, + UtsavFeedback } from '../models/associations.js'; import { createPendingTransaction, @@ -28,6 +35,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 +541,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.start_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, ROOM_STATUS_CHECKEDIN] + } + }); + + 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 new file mode 100644 index 00000000..960adedd --- /dev/null +++ b/migrations/20260201035615-add-utsav-feedback-model.js @@ -0,0 +1,128 @@ +'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, + allowNull: false, + references: { + model: 'card_db', + key: 'cardno' + }, + onDelete: 'CASCADE', + onUpdate: 'CASCADE' + }, + + utsav_id: { + type: Sequelize.INTEGER, + allowNull: false, + references: { + model: 'utsav_db', + key: 'id' + }, + onDelete: 'CASCADE', + onUpdate: 'CASCADE' + }, + + accommodation_rating: { + type: Sequelize.INTEGER, + allowNull: false + }, + + qr_rating: { + type: Sequelize.INTEGER, + allowNull: false + }, + + food_rating: { + type: Sequelize.INTEGER, + allowNull: false + }, + + program_rating: { + type: Sequelize.INTEGER, + allowNull: false + }, + + volunteer_rating: { + type: Sequelize.INTEGER, + allowNull: false + }, + + infrastructure_rating: { + type: Sequelize.INTEGER, + allowNull: false + }, + + decor_rating: { + type: Sequelize.INTEGER, + allowNull: false + }, + + internal_transport_rating: { + type: Sequelize.INTEGER, + allowNull: false + }, + + raj_pravas_rating: { + type: Sequelize.INTEGER, + allowNull: false + }, + + sparsh_rating: { + type: Sequelize.INTEGER, + allowNull: false + }, + + av_rating: { + type: Sequelize.INTEGER, + allowNull: false + }, + + loved_most: { + type: Sequelize.TEXT, + allowNull: false + }, + + improvement_suggestions: { + type: Sequelize.TEXT, + allowNull: false + }, + + createdAt: { + type: Sequelize.DATE, + allowNull: false, + defaultValue: Sequelize.literal('CURRENT_TIMESTAMP') + }, + + updatedAt: { + type: Sequelize.DATE, + allowNull: false, + defaultValue: Sequelize.literal( + 'CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP' + ) + } + }); + + await queryInterface.addConstraint('utsav_feedback', { + fields: ['utsav_id', 'cardno'], + type: 'unique', + name: 'unique_feedback_per_user_per_utsav' + }); + + 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..8ce86fc5 100644 --- a/models/associations.js +++ b/models/associations.js @@ -35,7 +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 ShibirAttendanceDb from './shibir_attendance_db.model.js'; +import UtsavFeedback from './utsav_feedback.model.js'; // CardDb CardDb.hasMany(GateRecord, { @@ -119,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', @@ -362,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' @@ -502,7 +515,6 @@ ShibirAttendanceDb.belongsTo(ShibirBookingDb, { targetKey: 'bookingid' }); - export { CardDb, Transactions, @@ -541,5 +553,6 @@ export { Updates, AdhyayanFeedback, RazorpaySettlementRecon, - ShibirAttendanceDb + 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 new file mode 100644 index 00000000..fd171535 --- /dev/null +++ b/models/utsav_feedback.model.js @@ -0,0 +1,156 @@ +import { DataTypes } from 'sequelize'; +import sequelize from '../config/database.js'; + +const UtsavFeedback = sequelize.define( + 'UtsavFeedback', + { + id: { + type: DataTypes.INTEGER, + primaryKey: true, + autoIncrement: true + }, + 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 + } + }, + { + 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 8a04b852..547f58e7 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,7 @@ 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;