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) {
Comment on lines +19 to +23
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

getAllRooms does not return live occupancy/status fields.

The current response returns only static room docs, so it does not satisfy the required live fields (status, current_event, occupied_until) for room status APIs.

💡 Proposed fix (shape)
 exports.getAllRooms = async (_req, res) => {
   try {
-    const rooms = await Room.find({ is_active: true });
-    res.json(rooms);
+    const now = new Date();
+    const rooms = await Room.find({ is_active: true }).lean();
+    const activeBookings = await RoomBooking.find({
+      status: 'Approved',
+      startTime: { $lte: now },
+      endTime: { $gte: now },
+    }).populate('event', 'title').lean();
+
+    const activeByRoom = new Map(activeBookings.map(b => [String(b.room), b]));
+    const payload = rooms.map(room => {
+      const active = activeByRoom.get(String(room._id));
+      return {
+        ...room,
+        status: active ? 'occupied' : 'vacant',
+        current_event: active?.event?.title ?? null,
+        occupied_until: active?.endTime ?? null,
+      };
+    });
+    res.json(payload);
   } catch (err) {
     res.status(500).json({ message: 'Error fetching rooms' });
   }
 };
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@backend/controllers/roomBookingController.js` around lines 19 - 23,
getAllRooms currently returns only static Room docs from Room.find and must be
extended to include live status fields (status, current_event, occupied_until).
After fetching rooms in getAllRooms, retrieve live occupancy data (from your
RoomStatus/Occupancy store or service) for each room (e.g., via
RoomStatus.findOne({ roomId }) or RoomStatusService.getStatus(room.id)), merge
those fields into each room object, and then send the augmented array with
res.json; perform the per-room lookups concurrently with Promise.all to avoid
serial waits and handle missing status records by supplying sensible defaults.

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 } },
],
});
Comment on lines +31 to +39
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 | 🔴 Critical

Clash detection is incorrect: it ignores booking date and accepts invalid time ranges.

This can report clashes across different days and allows startTime >= endTime.

💡 Proposed fix
   try {
     const { roomId, eventId, date, startTime, endTime, purpose } = req.body;
+    const bookingDate = new Date(date);
+    const start = new Date(startTime);
+    const end = new Date(endTime);
+
+    if ([bookingDate, start, end].some(d => Number.isNaN(d.getTime()))) {
+      return res.status(400).json({ message: 'Invalid date/time format' });
+    }
+    if (start >= end) {
+      return res.status(400).json({ message: 'startTime must be before endTime' });
+    }
+
     // Check for clash
     const clash = await RoomBooking.findOne({
       room: roomId,
+      date: bookingDate,
       status: { $in: ['Pending', 'Approved'] },
-      $or: [
-        { startTime: { $lt: endTime }, endTime: { $gt: startTime } },
-      ],
+      startTime: { $lt: end },
+      endTime: { $gt: start },
     });
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@backend/controllers/roomBookingController.js` around lines 31 - 39, The clash
logic in the RoomBooking.findOne query ignores the booking date and doesn’t
validate time ranges; update the controller to first validate that startTime <
endTime (reject requests where startTime >= endTime) and then include the
booking date in the query (e.g., filter by date or by combining date+time) so
only bookings on the same date are checked; keep the existing room: roomId and
status: { $in: ['Pending','Approved'] } filters and use the same overlap
condition ({ startTime: { $lt: endTime }, endTime: { $gt: startTime } }) but
applied together with date to prevent cross-day matches.

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);
Comment on lines +65 to +66
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

Avoid returning full bookedBy user documents to all authenticated callers.

Both endpoints currently populate full user objects, which can leak internal/sensitive fields unnecessarily.

💡 Proposed fix
-const bookings = await RoomBooking.find(query).populate('room event bookedBy');
+const bookings = await RoomBooking.find(query).populate('room event').populate({
+  path: 'bookedBy',
+  select: '_id role personal_info.name personal_info.email',
+});

Also applies to: 121-122

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@backend/controllers/roomBookingController.js` around lines 65 - 66, The
populated bookedBy user documents are exposing full user data; update both
populate calls (the one using RoomBooking.find(...).populate('room event
bookedBy') and the similar populate at lines ~121-122) to restrict fields by
using the object form: populate({ path: 'bookedBy', select: '_id name email' })
(or whichever minimal safe fields your User model exposes), e.g., replace the
string 'bookedBy' with the object selector so only safe user fields are returned
while keeping room and event populations unchanged.

} catch (err) {
res.status(500).json({ message: 'Error fetching availability' });
}
};


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 }
);
Comment on lines +77 to +84
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

Approval should re-check for overlapping approved bookings before status update.

Without a final overlap check on approve, edge/race scenarios can still end in double-approved slots.

💡 Proposed fix
   const { id } = req.params;
   const { status } = req.body;
@@
+  const current = await RoomBooking.findById(id);
+  if (!current) return res.status(404).json({ message: 'Booking not found' });
+
+  if (status === 'Approved') {
+    const approvedConflict = await RoomBooking.findOne({
+      _id: { $ne: current._id },
+      room: current.room,
+      date: current.date,
+      status: 'Approved',
+      startTime: { $lt: current.endTime },
+      endTime: { $gt: current.startTime },
+    });
+    if (approvedConflict) {
+      return res.status(409).json({ message: 'Cannot approve due to room clash' });
+    }
+  }
+
   const booking = await RoomBooking.findByIdAndUpdate(
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@backend/controllers/roomBookingController.js` around lines 77 - 84, Before
updating to 'Approved', load the target booking (use RoomBooking.findById(id))
to get its room and time window, then if status === 'Approved' run a query for
any other booking with status 'Approved' for the same room that overlaps the
target booking's start/end (e.g. findOne({ _id: { $ne: id }, room: booking.room,
status: 'Approved', $or: [{ start: { $lt: booking.end, $gte: booking.start } },
{ end: { $gt: booking.start, $lte: booking.end } }, { start: { $lte:
booking.start }, end: { $gte: booking.end } }] }) ) and if one exists return a
409/conflict; only then perform the update (RoomBooking.findByIdAndUpdate or
findOneAndUpdate). Ensure you reference RoomBooking.findById and
RoomBooking.findByIdAndUpdate in the change.

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,
},
],
Comment on lines +648 to +666
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

Room schema is missing required API fields (room_id, allowed_roles).

The linked API contract expects these fields in room responses, but the model currently does not store them.

💡 Proposed fix
 const roomSchema = new mongoose.Schema({
+  room_id: {
+    type: String,
+    required: true,
+    unique: true,
+    index: true,
+  },
   name: {
     type: String,
     required: true,
     unique: true,
   },
@@
   amenities: [
@@
   ],
+  allowed_roles: [
+    {
+      type: String,
+    },
+  ],
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@backend/models/schema.js` around lines 648 - 666, The roomSchema is missing
the API-required fields room_id and allowed_roles; update the mongoose
roomSchema to add a room_id field (String, required, unique — generated if
absent via a default function or a pre('save') hook using UUID logic) and an
allowed_roles field (Array of String, required or with a sensible default like
[]), and ensure any creation/update code that constructs Room documents (e.g.,
create handlers or Model.create usages) no longer omit room_id so responses
match the API contract; keep the symbol name roomSchema and adjust
indexes/validation as needed.

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,
},
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);

Comment on lines +11 to +13
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

Route contract does not match the required room APIs.

With the /api/rooms mount, GET /rooms becomes GET /api/rooms/rooms, and there is no GET /api/rooms/:room_id endpoint for room detail/status.

💡 Proposed fix
-// Get all rooms
-router.get('/rooms', isAuthenticated, roomBookingController.getAllRooms);
+// GET /api/rooms
+router.get('/', isAuthenticated, roomBookingController.getAllRooms);
+
+// GET /api/rooms/:room_id
+router.get('/:room_id', isAuthenticated, roomBookingController.getRoomById);

Also applies to: 35-35

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@backend/routes/roomBooking.js` around lines 11 - 13, The route paths are
incorrect: change router.get('/rooms', isAuthenticated,
roomBookingController.getAllRooms) to router.get('/', isAuthenticated,
roomBookingController.getAllRooms) so the mounted /api/rooms yields GET
/api/rooms; add a new parameterized route router.get('/:room_id',
isAuthenticated, roomBookingController.getRoomById) (or the appropriate
controller method that returns room details/status) to expose GET
/api/rooms/:room_id; also update the other similar occurrence (the one around
line 35) to use '/' or '/:room_id' instead of duplicating "rooms". Ensure the
referenced controller method name matches an exported function in
roomBookingController.

// 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 called, so rooms are never seeded.

This leaves the room booking flow without baseline room data after a fresh seed run.

💡 Proposed fix
 async function seedDB() {
   try {
@@
     await clearData();
+    await seedRooms();
     await seedOrganizationalUnits();
     await seedUsers();
🤖 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 sampleRooms are not inserted; call seedRooms from the main
seeding flow (for example inside the top-level async seed/run function or after
other seed calls) or add it to the Promise.all/await chain that runs other seed
helpers so it executes during seeding; locate the seedRooms function and ensure
the script invokes seedRooms() (or exports and calls it where other seeds like
seedUsers/seedBookings are executed) so rooms get inserted on a fresh seed.


// --- Data for Seeding ---

// Original club/committee data.
Expand Down