Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
126 changes: 126 additions & 0 deletions backend/controllers/roomBookingController.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,126 @@

const { Room, RoomBooking, Event, User } = require('../models/schema');

exports.createRoom = async (req, res) => {
try {
const { name, capacity, location, amenities } = req.body;
const room = new Room({ name, capacity, location, amenities });
await room.save();
res.status(201).json({ message: 'Room created', room });
} catch (err) {
if (err.code === 11000) {
return res.status(409).json({ message: 'Room name already exists' });
}
res.status(500).json({ message: 'Error creating room', error: err.message });
}
};


exports.getAllRooms = async (_req, res) => {
try {
const rooms = await Room.find({ is_active: true });
res.json(rooms);
} catch (err) {
res.status(500).json({ message: 'Error fetching rooms' });
}
};


exports.bookRoom = async (req, res) => {
try {
const { roomId, eventId, date, startTime, endTime, purpose } = req.body;
// Check for clash
const clash = await RoomBooking.findOne({
room: roomId,
status: { $in: ['Pending', 'Approved'] },
$or: [
{ startTime: { $lt: endTime }, endTime: { $gt: startTime } },
],
});
if (clash) {
return res.status(409).json({ message: 'Room clash detected', conflictingBooking: clash });
}
const booking = new RoomBooking({
room: roomId,
event: eventId,
date,
startTime,
endTime,
purpose,
bookedBy: req.user._id,
});
await booking.save();
res.status(201).json({ message: 'Room booked (pending approval)', booking });
} catch (err) {
res.status(500).json({ message: 'Error booking room', error: err.message });
}
};


exports.getAvailability = async (req, res) => {
try {
const { date, roomId } = req.query;
const query = { date: new Date(date) };
if (roomId) query.room = roomId;
const bookings = await RoomBooking.find(query).populate('room event bookedBy');
res.json(bookings);
} catch (err) {
res.status(500).json({ message: 'Error fetching availability' });
}
};
Comment on lines +60 to +70
Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

Date matching may fail due to time component differences.

The exact date match { date: new Date(date) } is fragile. If the stored date has a different time component (e.g., due to timezone handling), the query won't match. Consider using a date range query.

🛡️ Proposed fix using date range
 exports.getAvailability = async (req, res) => {
   try {
     const { date, roomId } = req.query;
-    const query = { date: new Date(date) };
+    const queryDate = new Date(date);
+    const nextDate = new Date(queryDate);
+    nextDate.setDate(nextDate.getDate() + 1);
+    const query = { date: { $gte: queryDate, $lt: nextDate } };
     if (roomId) query.room = roomId;
     const bookings = await RoomBooking.find(query).populate('room event bookedBy');
     res.json(bookings);
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
exports.getAvailability = async (req, res) => {
try {
const { date, roomId } = req.query;
const query = { date: new Date(date) };
if (roomId) query.room = roomId;
const bookings = await RoomBooking.find(query).populate('room event bookedBy');
res.json(bookings);
} catch (err) {
res.status(500).json({ message: 'Error fetching availability' });
}
};
exports.getAvailability = async (req, res) => {
try {
const { date, roomId } = req.query;
const queryDate = new Date(date);
const nextDate = new Date(queryDate);
nextDate.setDate(nextDate.getDate() + 1);
const query = { date: { $gte: queryDate, $lt: nextDate } };
if (roomId) query.room = roomId;
const bookings = await RoomBooking.find(query).populate('room event bookedBy');
res.json(bookings);
} catch (err) {
res.status(500).json({ message: 'Error fetching availability' });
}
};
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@backend/controllers/roomBookingController.js` around lines 60 - 70, The
getAvailability handler uses an exact date match (query = { date: new Date(date)
}) which fails when stored dates include time components; update getAvailability
to build a date range instead: parse the incoming date into a start-of-day and
end-of-day (e.g., start.setHours(0,0,0,0) and end = new Date(start);
end.setDate(end.getDate()+1)) and replace the equality with a range query on
RoomBooking.find (e.g., { date: { $gte: start, $lt: end } }), keeping the
existing optional roomId branch and populate('room event bookedBy') behavior.



exports.updateBookingStatus = async (req, res) => {
try {
const { id } = req.params;
const { status } = req.body;
if (!['Approved', 'Rejected'].includes(status)) {
return res.status(400).json({ message: 'Invalid status' });
}
const booking = await RoomBooking.findByIdAndUpdate(
id,
{ status, reviewedBy: req.user._id, updated_at: new Date() },
{ new: true }
);
if (!booking) return res.status(404).json({ message: 'Booking not found' });
res.json({ message: 'Booking status updated', booking });
} catch (err) {
res.status(500).json({ message: 'Error updating booking status' });
}
};


exports.cancelBooking = async (req, res) => {
try {
const { id } = req.params;
const booking = await RoomBooking.findById(id);
if (!booking) return res.status(404).json({ message: 'Booking not found' });

if (
String(booking.bookedBy) !== String(req.user._id) &&
!['PRESIDENT', 'GENSEC_SCITECH', 'GENSEC_ACADEMIC', 'GENSEC_CULTURAL', 'GENSEC_SPORTS', 'CLUB_COORDINATOR'].includes(req.user.role)
) {
return res.status(403).json({ message: 'Forbidden' });
}
booking.status = 'Cancelled';
booking.updated_at = new Date();
await booking.save();
res.json({ message: 'Booking cancelled', booking });
} catch (err) {
res.status(500).json({ message: 'Error cancelling booking' });
}
};

exports.getBookings = async (req, res) => {
try {
const { roomId, date, status } = req.query;
const query = {};
if (roomId) query.room = roomId;
if (date) query.date = new Date(date);
if (status) query.status = status;
const bookings = await RoomBooking.find(query).populate('room event bookedBy');
res.json(bookings);
} catch (err) {
res.status(500).json({ message: 'Error fetching bookings' });
}
};
2 changes: 2 additions & 0 deletions backend/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ const dashboardRoutes = require("./routes/dashboard.js");

const analyticsRoutes = require("./routes/analytics.js");
const porRoutes = require("./routes/por.js");
const roomBookingRoutes = require("./routes/roomBooking.js");
const app = express();

if (process.env.NODE_ENV === "production") {
Expand Down Expand Up @@ -68,6 +69,7 @@ app.use("/api/announcements", announcementRoutes);
app.use("/api/dashboard", dashboardRoutes);
app.use("/api/announcements", announcementRoutes);
app.use("/api/analytics", analyticsRoutes);
app.use("/api/rooms", roomBookingRoutes);
app.use("/api/por", porRoutes);

// Start the server
Expand Down
90 changes: 90 additions & 0 deletions backend/models/schema.js
Original file line number Diff line number Diff line change
Expand Up @@ -645,6 +645,94 @@ const OrganizationalUnit = mongoose.model(
);
const Announcement = mongoose.model("Announcement", announcementSchema);

const roomSchema = new mongoose.Schema({
name: {
type: String,
required: true,
unique: true,
},
capacity: {
type: Number,
required: true,
},
location: {
type: String,
required: true,
},
amenities: [
{
type: String,
},
],
is_active: {
type: Boolean,
default: true,
},
created_at: {
type: Date,
default: Date.now,
},
updated_at: {
type: Date,
default: Date.now,
},
});

const Room = mongoose.model("Room", roomSchema);

const roomBookingSchema = new mongoose.Schema({
room: {
type: mongoose.Schema.Types.ObjectId,
ref: "Room",
required: true,
},
event: {
type: mongoose.Schema.Types.ObjectId,
ref: "Event",
},
date: {
type: Date,
required: true,
},
startTime: {
type: Date,
required: true,
},
endTime: {
type: Date,
required: true,
},
Comment on lines +697 to +704
Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Verify whether any temporal guard already exists in model/controller paths.
rg -nP -C3 'startTime|endTime|validate|bookRoom|getAvailability' backend/models/schema.js backend/controllers/roomBookingController.js

Repository: OpenLake/Student_Database_COSA

Length of output: 3022


Add schema validation: endTime must be after startTime.

Invalid booking windows (e.g., endTime ≤ startTime) can be persisted, breaking the clash detection logic in bookRoom. The overlap query assumes valid time windows and would silently fail to detect conflicts.

💡 Proposed fix
 const roomBookingSchema = new mongoose.Schema({
@@
   endTime: {
     type: Date,
     required: true,
   },
@@
 });
 
+roomBookingSchema.path("endTime").validate(function (value) {
+  return this.startTime instanceof Date && value instanceof Date && value > this.startTime;
+}, "endTime must be later than startTime");
+
 roomBookingSchema.index({ room: 1, date: 1, startTime: 1, endTime: 1 });
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@backend/models/schema.js` around lines 697 - 704, Add a schema-level
validation to ensure endTime > startTime on the booking schema: in
backend/models/schema.js add a custom validator (either on the endTime field or
via a pre('validate') hook) that compares this.endTime and this.startTime and
rejects/surfaces a validation error when endTime is <= startTime; reference the
booking schema (e.g., BookingSchema or the schema that defines startTime and
endTime) so saves/creates fail early and the bookRoom overlap logic no longer
assumes invalid windows.

purpose: {
type: String,
},
bookedBy: {
type: mongoose.Schema.Types.ObjectId,
ref: "User",
required: true,
},
status: {
type: String,
enum: ["Pending", "Approved", "Rejected", "Cancelled"],
default: "Pending",
},
reviewedBy: {
type: mongoose.Schema.Types.ObjectId,
ref: "User",
},
created_at: {
type: Date,
default: Date.now,
},
updated_at: {
type: Date,
default: Date.now,
},
});

roomBookingSchema.index({ room: 1, date: 1, startTime: 1, endTime: 1 });

const RoomBooking = mongoose.model("RoomBooking", roomBookingSchema);

module.exports = {
User,
Feedback,
Expand All @@ -656,4 +744,6 @@ module.exports = {
Position,
OrganizationalUnit,
Announcement,
Room,
RoomBooking,
};
35 changes: 35 additions & 0 deletions backend/routes/roomBooking.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
const express = require('express');
const router = express.Router();
const isAuthenticated = require('../middlewares/isAuthenticated');
const authorizeRole = require('../middlewares/authorizeRole');
const { ROLE_GROUPS, ROLES } = require('../utils/roles');
const roomBookingController = require('../controllers/roomBookingController');

// Create a new room (admin only)
router.post('/create-room', isAuthenticated, authorizeRole(ROLE_GROUPS.ADMIN), roomBookingController.createRoom);

// Get all rooms
router.get('/rooms', isAuthenticated, roomBookingController.getAllRooms);

// Book a room (admin only)
router.post('/book', isAuthenticated, authorizeRole(ROLE_GROUPS.ADMIN), roomBookingController.bookRoom);

// Get room availability
router.get('/availability', isAuthenticated, roomBookingController.getAvailability);

// Get bookings (filterable)
router.get('/bookings', isAuthenticated, roomBookingController.getBookings);

// Update booking status (approve/reject)
router.put('/bookings/:id/status', isAuthenticated, authorizeRole([
ROLES.PRESIDENT,
ROLES.GENSEC_SCITECH,
ROLES.GENSEC_ACADEMIC,
ROLES.GENSEC_CULTURAL,
ROLES.GENSEC_SPORTS,
]), roomBookingController.updateBookingStatus);

// Cancel a booking
router.delete('/bookings/:id', isAuthenticated, roomBookingController.cancelBooking);

module.exports = router;
35 changes: 26 additions & 9 deletions backend/seed.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,17 +2,34 @@ require("dotenv").config();
const mongoose = require("mongoose");

const {
User,
Feedback,
Achievement,
UserSkill,
Skill,
Event,
PositionHolder,
Position,
OrganizationalUnit,
User,
Feedback,
Achievement,
UserSkill,
Skill,
Event,
PositionHolder,
Position,
OrganizationalUnit,
Room,
} = require("./models/schema");

// Sample Rooms for Seeding
const sampleRooms = [
{ name: "LH-101", capacity: 60, location: "Academic Block 1, Ground Floor", amenities: ["Projector", "AC", "Whiteboard"] },
{ name: "LH-102", capacity: 60, location: "Academic Block 1, Ground Floor", amenities: ["Projector", "AC"] },
{ name: "Seminar Hall", capacity: 120, location: "Admin Block, 1st Floor", amenities: ["Projector", "Sound System", "AC"] },
];

// Seeds sample rooms for testing room booking features.

const seedRooms = async () => {
console.log("Seeding sample rooms...");
await Room.deleteMany({});
await Room.insertMany(sampleRooms);
console.log("Sample rooms seeded!");
};
Comment on lines +26 to +31
Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

seedRooms is never executed, so room data is never seeded.

seedRooms is defined on Line 26 but not called in seedDB(); this leaves fresh environments without room records for booking workflows.

💡 Proposed fix
 async function seedDB() {
   try {
@@
     await clearData();
     await seedOrganizationalUnits();
+    await seedRooms();
     await seedUsers();
     await seedPositions();
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@backend/seed.js` around lines 26 - 31, The seedRooms function is defined but
never invoked, so room records aren't created; update the seedDB function to
call seedRooms (preferably await seedRooms() inside the async seedDB) or invoke
seedRooms() where other seeding functions are run so the sampleRooms are
inserted; ensure the call uses await if seedDB is async to preserve ordering and
error propagation and reference the seedRooms symbol when adding the call.


// --- Data for Seeding ---

// Original club/committee data.
Expand Down
Loading