Skip to content
Closed
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
42 changes: 36 additions & 6 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,9 @@ on:
branches: ["master"]

jobs:
# ─────────────────────────────────────────────
# Backend CI — lint, unit tests, integration tests
# ─────────────────────────────────────────────
backend:
name: Backend Tests
runs-on: ubuntu-latest
Expand All @@ -26,16 +29,20 @@ jobs:
working-directory: backend
run: npm ci

- name: Run backend tests
- name: Lint backend
working-directory: backend
run: npm test || echo "No backend tests defined yet"
run: npx eslint . || echo "Lint warnings found"

- name: Verify backend build
- name: Run backend tests
working-directory: backend
run: npm run build || echo "No backend build step defined"
continue-on-error: true
run: npm test -- --verbose

# ─────────────────────────────────────────────
# Frontend CI — lint, unit tests, build
# ─────────────────────────────────────────────
frontend:
name: Frontend Build Test
name: Frontend Build & Test
runs-on: ubuntu-latest

steps:
Expand All @@ -53,10 +60,33 @@ jobs:
working-directory: frontend
run: npm ci

- name: Lint frontend
working-directory: frontend
run: npm run lint

- name: Run frontend tests
working-directory: frontend
run: npm test

- name: Build frontend
working-directory: frontend
run: npm run build
run: npm run build

# ─────────────────────────────────────────────
# Deploy Frontend to Vercel (only on master push)
# ─────────────────────────────────────────────
deploy-frontend:
name: Deploy Frontend
needs: [frontend]
if: github.event_name == 'push' && github.ref == 'refs/heads/master'
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4

- name: Deploy to Vercel
uses: amondnet/vercel-action@v25
with:
vercel-token: ${{ secrets.VERCEL_TOKEN }}
vercel-org-id: ${{ secrets.VERCEL_ORG_ID }}
vercel-project-id: ${{ secrets.VERCEL_PROJECT_ID }}
vercel-args: '--prod'
181 changes: 176 additions & 5 deletions backend/controllers/dashboard.controller.js
Original file line number Diff line number Diff line change
@@ -1,20 +1,191 @@
import Faculty from '../models/faculty.js';
import Course from '../models/course.js';
import Classroom from '../models/classroom.js';
import CourseAssignment from '../models/courseAssignment.js';
import SlotChangeRequest from '../models/slotChangeRequest.js';

// GET: Dashboard statistics
// GET: Comprehensive dashboard analytics
export const getDashboardStats = async (req, res) => {
try {
const facultyCount = await Faculty.countDocuments();
const courseCount = await Course.countDocuments();
const roomCount = await Classroom.countDocuments();
// --- 1. Summary Counts ---
const [facultyCount, courseCount, roomCount, timetableCount, pendingRequests] = await Promise.all([
Faculty.countDocuments(),
Course.countDocuments(),
Classroom.countDocuments(),
CourseAssignment.countDocuments({ isActive: true }),
SlotChangeRequest.countDocuments({ status: { $in: ['Pending_Faculty', 'Pending_Admin'] } })
]);

// --- 2. Faculty Analytics ---
const [facultyByDept, facultyByDesignation, facultyByType] = await Promise.all([
Faculty.aggregate([
{ $group: { _id: '$department', count: { $sum: 1 } } },
{ $sort: { count: -1 } }
]),
Faculty.aggregate([
{ $group: { _id: '$designation', count: { $sum: 1 } } },
{ $sort: { count: -1 } }
]),
Faculty.aggregate([
{ $group: { _id: '$facultyType', count: { $sum: 1 } } },
{ $sort: { count: -1 } }
])
]);

// --- 3. Course Analytics ---
const [coursesByDept, coursesByType, creditDistribution] = await Promise.all([
Course.aggregate([
{ $group: { _id: '$department', count: { $sum: 1 } } },
{ $sort: { count: -1 } }
]),
Course.aggregate([
{ $group: { _id: '$courseType', count: { $sum: 1 } } },
{ $sort: { count: -1 } }
]),
Course.aggregate([
{ $group: { _id: '$credits', count: { $sum: 1 } } },
{ $sort: { _id: 1 } }
])
]);

// --- 4. Room Analytics ---
const [roomsByType, roomsByBuilding, capacityStats] = await Promise.all([
Classroom.aggregate([
{ $group: { _id: '$roomType', count: { $sum: 1 } } },
{ $sort: { count: -1 } }
]),
Classroom.aggregate([
{ $group: { _id: '$building', count: { $sum: 1 } } },
{ $sort: { count: -1 } }
]),
Classroom.aggregate([
{ $group: {
_id: null,
totalCapacity: { $sum: '$capacity' },
avgCapacity: { $avg: '$capacity' },
maxCapacity: { $max: '$capacity' },
minCapacity: { $min: '$capacity' }
}}
])
]);

// --- 5. Timetable Slot Heatmap ---
const timetables = await CourseAssignment.find(
{ isActive: true },
{ timetableSlots: 1, department: 1, section: 1 }
);

const days = ['Monday', 'Tuesday', 'Wednesday', 'Thursday', 'Friday'];
const slotHeatmap = {};
let totalSlotsUsed = 0;

days.forEach(day => {
slotHeatmap[day] = {};
for (let s = 1; s <= 9; s++) {
slotHeatmap[day][s] = 0;
}
});

timetables.forEach(tt => {
(tt.timetableSlots || []).forEach(slot => {
if (slot.day && slot.slotNumber && slot.slotNumber <= 9 && !slot.isSpanContinuation) {
if (slot.slotType !== 'Break') {
slotHeatmap[slot.day] = slotHeatmap[slot.day] || {};
slotHeatmap[slot.day][slot.slotNumber] = (slotHeatmap[slot.day][slot.slotNumber] || 0) + 1;
totalSlotsUsed++;
}
}
});
});

// Find busiest day and slot
let busiestDay = '', busiestDayCount = 0;
let busiestSlot = 0, busiestSlotCount = 0;

days.forEach(day => {
const dayTotal = Object.values(slotHeatmap[day] || {}).reduce((a, b) => a + b, 0);
if (dayTotal > busiestDayCount) {
busiestDay = day;
busiestDayCount = dayTotal;
}
for (let s = 1; s <= 9; s++) {
if ((slotHeatmap[day]?.[s] || 0) > busiestSlotCount) {
busiestSlot = s;
busiestSlotCount = slotHeatmap[day][s];
}
}
});

// --- 6. Slot Change Request Analytics ---
const [requestsByStatus, recentRequests] = await Promise.all([
SlotChangeRequest.aggregate([
{ $group: { _id: '$status', count: { $sum: 1 } } }
]),
SlotChangeRequest.find()
.sort({ createdAt: -1 })
.limit(5)
.select('courseCode courseName facultyName status currentDay currentSlotNumber requestedDay requestedSlotNumber createdAt reason')
.lean()
]);

// --- 7. Department Workload (from timetable data) ---
const deptWorkload = {};
timetables.forEach(tt => {
const dept = tt.department || 'Unknown';
if (!deptWorkload[dept]) {
deptWorkload[dept] = { sections: new Set(), totalSlots: 0 };
}
deptWorkload[dept].sections.add(tt.section);
deptWorkload[dept].totalSlots += (tt.timetableSlots || []).filter(
s => s.slotType !== 'Break' && !s.isSpanContinuation
).length;
});

const departmentWorkload = Object.entries(deptWorkload).map(([dept, data]) => ({
department: dept,
sections: data.sections.size,
totalSlots: data.totalSlots,
avgSlotsPerSection: data.sections.size > 0 ? Math.round(data.totalSlots / data.sections.size) : 0
})).sort((a, b) => b.totalSlots - a.totalSlots);

res.status(200).json({
// Summary
facultyCount,
courseCount,
roomCount
roomCount,
timetableCount,
pendingRequests,

// Faculty
facultyByDept: facultyByDept.map(d => ({ name: d._id || 'Unknown', value: d.count })),
facultyByDesignation: facultyByDesignation.map(d => ({ name: d._id || 'Unknown', value: d.count })),
facultyByType: facultyByType.map(d => ({ name: d._id || 'Unknown', value: d.count })),

// Courses
coursesByDept: coursesByDept.map(d => ({ name: d._id || 'Unknown', value: d.count })),
coursesByType: coursesByType.map(d => ({ name: d._id || 'Unknown', value: d.count })),
creditDistribution: creditDistribution.map(d => ({ credits: d._id, count: d.count })),

// Rooms
roomsByType: roomsByType.map(d => ({ name: d._id || 'Unknown', value: d.count })),
roomsByBuilding: roomsByBuilding.map(d => ({ name: d._id || 'Unknown', value: d.count })),
capacityStats: capacityStats[0] || { totalCapacity: 0, avgCapacity: 0, maxCapacity: 0, minCapacity: 0 },

// Heatmap
slotHeatmap,
totalSlotsUsed,
busiestDay,
busiestSlot,

// Requests
requestsByStatus: requestsByStatus.map(d => ({ name: d._id || 'Unknown', value: d.count })),
recentRequests,

// Department workload
departmentWorkload
});
} catch (error) {
console.error('Dashboard stats error:', error);
res.status(500).json({ message: "Error fetching dashboard statistics" });
}
};
23 changes: 6 additions & 17 deletions backend/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -21,25 +21,14 @@ import StudentRoute from './routes/student.route.js';
const PORT = process.env.PORT;

const app = express();

app.use(express.json());
app.use(cookieParser());
const allowedOrigins = [
'http://localhost:5173',
process.env.FRONTEND_URL || 'https://timetable-app-taupe.vercel.app'
];

app.use(cors({
origin: function (origin, callback) {
// Allow requests with no origin (like mobile apps or curl)
if (!origin) return callback(null, true);
if (allowedOrigins.includes(origin)) {
return callback(null, true);
}
return callback(new Error('Not allowed by CORS'));
},
credentials: true
}));
app.use(cors(
{
origin: 'http://localhost:5173',
credentials: true
}
));

// Health check endpoint (used by Elastic Beanstalk for instance monitoring)
app.get('/health', (req, res) => {
Expand Down
Loading
Loading