Skip to content

Commit 599239e

Browse files
Merge pull request #146 from harshitap1305/design/dash
[SPRINT-M25] enhancement: created role based dashboard
2 parents eee2718 + 0ad8885 commit 599239e

34 files changed

Lines changed: 797 additions & 126 deletions
Lines changed: 187 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,187 @@
1+
// controllers/dashboardController.js
2+
const {
3+
Feedback,
4+
Achievement,
5+
UserSkill,
6+
Skill,
7+
Event,
8+
PositionHolder,
9+
Position,
10+
OrganizationalUnit,
11+
} = require("../models/schema");
12+
13+
const ROLES = {
14+
PRESIDENT: "PRESIDENT",
15+
GENSEC_SCITECH: "GENSEC_SCITECH",
16+
GENSEC_ACADEMIC: "GENSEC_ACADEMIC",
17+
GENSEC_CULTURAL: "GENSEC_CULTURAL",
18+
GENSEC_SPORTS: "GENSEC_SPORTS",
19+
CLUB_COORDINATOR: "CLUB_COORDINATOR",
20+
STUDENT: "STUDENT",
21+
};
22+
23+
const roleToCategoryMap = {
24+
[ROLES.GENSEC_SCITECH]: "scitech",
25+
[ROLES.GENSEC_ACADEMIC]: "academic",
26+
[ROLES.GENSEC_CULTURAL]: "cultural",
27+
[ROLES.GENSEC_SPORTS]: "sports",
28+
};
29+
30+
31+
exports.getDashboardStats = async (req, res) => {
32+
const { _id: userId, username, role } = req.user;
33+
const email =username;
34+
let stats = {};
35+
36+
try {
37+
switch (role.toUpperCase()) {
38+
39+
// STUDENT
40+
case ROLES.STUDENT: {
41+
const [totalSkills, totalFeedbacksGiven, totalAchievements, totalPORs] =
42+
await Promise.all([
43+
UserSkill.countDocuments({ user_id: userId }),
44+
Feedback.countDocuments({ feedback_by: userId }),
45+
Achievement.countDocuments({ user_id: userId }),
46+
PositionHolder.countDocuments({ user_id: userId, status: "active" }),
47+
]);
48+
49+
stats = {
50+
totalSkills,
51+
totalFeedbacksGiven,
52+
totalAchievements,
53+
totalPORs,
54+
};
55+
break;
56+
}
57+
58+
// ALL GENSECS (SciTech, Cultural, etc.)
59+
case ROLES.GENSEC_SCITECH:
60+
case ROLES.GENSEC_ACADEMIC:
61+
case ROLES.GENSEC_CULTURAL:
62+
case ROLES.GENSEC_SPORTS: {
63+
const roleCategory = roleToCategoryMap[role];
64+
if (!roleCategory) {
65+
return res.status(400).json({ msg: "Invalid GenSec role category." });
66+
}
67+
68+
// Find the GenSec's organizational unit (Council) by their email
69+
const orgUnit = await OrganizationalUnit.findOne({ "contact_info.email": email });
70+
if (!orgUnit) {
71+
console.error("Organizational Unit for GenSec not found.");
72+
return res.status(404).json({ msg: "Organizational Unit for GenSec not found." });
73+
}
74+
75+
// Query for pending user skills of the correct category using aggregation
76+
const pendingUserSkillsAggregation = await UserSkill.aggregate([
77+
{ $match: { is_endorsed: false } }, // Find un-endorsed user skills
78+
{
79+
$lookup: {
80+
from: "skills",
81+
localField: "skill_id",
82+
foreignField: "_id",
83+
as: "skillDoc",
84+
},
85+
},
86+
{ $unwind: "$skillDoc" },
87+
{ $match: { "skillDoc.type": roleCategory } },
88+
{ $count: "count" },
89+
]);
90+
91+
const [childClubsCount, pendingSkills, pendingAchievements] =
92+
await Promise.all([
93+
94+
OrganizationalUnit.countDocuments({ parent_unit_id: orgUnit._id }),
95+
Skill.countDocuments({ is_endorsed: false, type: roleCategory }),
96+
Achievement.countDocuments({ verified: false }),
97+
]);
98+
99+
stats = {
100+
budget: {
101+
used: orgUnit.budget_info.spent_amount,
102+
total: orgUnit.budget_info.allocated_budget,
103+
},
104+
parentOfClubs: childClubsCount,
105+
pendingSkillsEndorsement: pendingSkills,
106+
pendingUserSkillsEndorsement: (pendingUserSkillsAggregation.length > 0 ? pendingUserSkillsAggregation[0].count : 0),
107+
pendingAchievementEndorsement: pendingAchievements,
108+
};
109+
break;
110+
}
111+
112+
// CLUB COORDINATOR
113+
114+
case ROLES.CLUB_COORDINATOR: {
115+
const clubUnit = await OrganizationalUnit.findOne({ "contact_info.email": email });
116+
if (!clubUnit) {
117+
console.error("Club unit for Coordinator not found.");
118+
console.log(email);
119+
return res.status(404).json({ msg: "Club unit for Coordinator not found." });
120+
}
121+
122+
// Find all positions associated with this club
123+
const positionsInClub = await Position.find({ unit_id: clubUnit._id }).select('_id');
124+
const positionIds = positionsInClub.map(p => p._id);
125+
126+
// Find all active members holding those positions
127+
const activeMembers = await PositionHolder.find({
128+
position_id: { $in: positionIds },
129+
status: 'active'
130+
});
131+
const memberUserIds = activeMembers.map(m => m.user_id);
132+
133+
const [totalEvents, pendingReviews] = await Promise.all([
134+
Event.countDocuments({ organizing_unit_id: clubUnit._id }),
135+
Achievement.countDocuments({ user_id: { $in: memberUserIds }, verified: false }),
136+
]);
137+
138+
stats = {
139+
budget: {
140+
used: clubUnit.budget_info.spent_amount,
141+
total: clubUnit.budget_info.allocated_budget,
142+
},
143+
totalEvents,
144+
totalActiveMembers: activeMembers.length,
145+
pendingReviews,
146+
};
147+
break;
148+
}
149+
150+
151+
// PRESIDENT
152+
case ROLES.PRESIDENT: {
153+
const budgetAggregation = await OrganizationalUnit.aggregate([
154+
{ $group: {
155+
_id: null,
156+
totalBudget: { $sum: "$budget_info.allocated_budget" },
157+
usedBudget: { $sum: "$budget_info.spent_amount" }
158+
}}
159+
]);
160+
161+
const [pendingRoomRequests, totalOrgUnits] = await Promise.all([
162+
Event.countDocuments({ "room_requests.status": "Pending" }),
163+
OrganizationalUnit.countDocuments()
164+
]);
165+
166+
stats = {
167+
pendingRoomRequests,
168+
totalOrgUnits,
169+
budget: {
170+
used: (budgetAggregation[0] && budgetAggregation[0].usedBudget) || 0,
171+
total: (budgetAggregation[0] && budgetAggregation[0].totalBudget) || 0,
172+
}
173+
};
174+
break;
175+
}
176+
177+
default:
178+
return res.status(400).json({ msg: "Invalid user role for stats." });
179+
}
180+
181+
res.status(200).json(stats);
182+
183+
} catch (error) {
184+
console.error("Error fetching dashboard stats:", error);
185+
res.status(500).send("Server Error");
186+
}
187+
};
Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
const {Event} = require('../models/schema');
2+
3+
// fetch 4 most recently updated events
4+
exports.getLatestEvents = async (req, res) => {
5+
try{
6+
const latestEvents = await Event.find({})
7+
.sort({updated_at: -1})
8+
.limit(4)
9+
.select('title updated_at schedule.venue status');
10+
11+
const formatedEvents =latestEvents.map(event=>({
12+
id: event._id,
13+
title: event.title,
14+
date: event.updated_at.toLocaleDateString('en-US', { month: 'short', day: 'numeric' }),
15+
venue: (event.schedule && event.schedule.venue) ? event.schedule.venue : 'TBA',
16+
status: event.status || 'TBD'
17+
}))
18+
res.status(200).json(formatedEvents);
19+
} catch (error) {
20+
console.error('Error fetching latest events:', error);
21+
res.status(500).json({ message: 'Server error' });
22+
}
23+
};

backend/index.js

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@ const skillsRoutes = require("./routes/skillsRoutes.js");
1616
const achievementsRoutes = require("./routes/achievements.js");
1717
const positionsRoutes = require("./routes/positionRoutes.js");
1818
const organizationalUnitRoutes = require("./routes/orgUnit.js");
19-
19+
const dashboardRoutes = require("./routes/dashboard.js");
2020
const app = express();
2121
app.use(cors({ origin: process.env.FRONTEND_URL, credentials: true }));
2222

@@ -51,6 +51,7 @@ app.use("/api/skills", skillsRoutes);
5151
app.use("/api/achievements", achievementsRoutes);
5252
app.use("/api/positions", positionsRoutes);
5353
app.use("/api/orgUnit", organizationalUnitRoutes);
54+
app.use("/api/dashboard", dashboardRoutes);
5455

5556
// Start the server
5657
app.listen(process.env.PORT || 8000, () => {

backend/routes/dashboard.js

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
const express = require('express');
2+
const router = express.Router();
3+
const dashboardController = require('../controllers/dashboardController');
4+
const isAuthenticated = require('../middlewares/isAuthenticated');
5+
6+
router.get('/stats',isAuthenticated, dashboardController.getDashboardStats);
7+
8+
module.exports = router;

backend/routes/events.js

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,10 @@ const isAuthenticated = require("../middlewares/isAuthenticated");
66
const isEventContact = require("../middlewares/isEventContact");
77
const authorizeRole = require("../middlewares/authorizeRole");
88
const { ROLE_GROUPS, ROLES } = require("../utils/roles");
9+
const eventsController = require("../controllers/eventControllers");
10+
11+
12+
router.get("/latest", eventsController.getLatestEvents);
913

1014
// Create a new event (new events can be created by admins only)
1115
router.post(

frontend/src/Components/Auth/RoleRedirect.jsx

Lines changed: 1 addition & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -19,23 +19,7 @@ const RoleRedirect = () => {
1919
if (!userRole) {
2020
return <div>Loading user role...</div>; // Or just return null for a blank screen
2121
}
22-
23-
switch (userRole) {
24-
case "PRESIDENT":
25-
return <Navigate to="/president-dashboard" replace />;
26-
case "GENSEC_SCITECH":
27-
return <Navigate to="/gensec-tech" replace />;
28-
case "GENSEC_ACADEMIC":
29-
return <Navigate to="/gensec-acad" replace />;
30-
case "GENSEC_CULTURAL":
31-
return <Navigate to="/gensec-cult" replace />;
32-
case "GENSEC_SPORTS":
33-
return <Navigate to="/gensec-sport" replace />;
34-
case "CLUB_COORDINATOR":
35-
return <Navigate to="/club-dashboard" replace />;
36-
default:
37-
return <Navigate to="/home" replace />; // fallback for student
38-
}
22+
return <Navigate to="/dashboard" replace />;
3923
};
4024

4125
export default RoleRedirect;
Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
// src/components/dashboard/cards/common/StatCard.js
2+
import React from "react";
3+
4+
const BaseStatCard = ({ title, value, footer }) => (
5+
<div className="p-4 bg-white/90 rounded-2xl shadow-sm h-full flex flex-col justify-between">
6+
<div>
7+
<div className="text-xs text-slate-600 uppercase tracking-wider">{title}</div>
8+
<div className="text-3xl font-bold text-slate-900 mt-1">{value}</div>
9+
</div>
10+
{footer && <div className="text-sm text-green-600 mt-2">{footer}</div>}
11+
</div>
12+
);
13+
14+
export default BaseStatCard;
Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,51 @@
1+
import React from 'react';
2+
3+
// A small component to visually represent event status
4+
const StatusIndicator = ({ status }) => {
5+
const statusStyles = {
6+
planned: "bg-blue-500",
7+
ongoing: "bg-green-500",
8+
completed: "bg-slate-500",
9+
cancelled: "bg-red-500",
10+
};
11+
const colorClass = statusStyles[status] || "bg-gray-400";
12+
13+
return (
14+
<div className="flex items-center gap-1.5">
15+
<div className={`w-2 h-2 rounded-full ${colorClass}`}></div>
16+
<span className="capitalize text-xs">{status}</span>
17+
</div>
18+
);
19+
};
20+
21+
const UpdatesCard = ({ updates }) => {
22+
return (
23+
<div className="p-4 bg-white/90 rounded-2xl shadow-sm">
24+
<h3 className="text-base font-semibold mb-3">Latest Updates</h3>
25+
26+
{!updates || updates.length === 0 ? (
27+
<p className="text-sm text-slate-500">No recent updates found.</p>
28+
) : (
29+
<ul className="space-y-3 p-0 list-none">
30+
{updates.map((update) => (
31+
<li key={update.id} className="bg-white/70 p-2 rounded-xl shadow-sm">
32+
33+
<div className="flex items-start justify-between">
34+
<p className="font-medium text-sm pr-2">{update.title}</p>
35+
<p className="text-xs text-slate-700 whitespace-nowrap">{update.date}</p>
36+
</div>
37+
38+
<div className="flex items-center justify-between text-slate-600">
39+
<p className="text-xs">Venue: {update.venue}</p>
40+
<StatusIndicator status={update.status} />
41+
</div>
42+
43+
</li>
44+
))}
45+
</ul>
46+
)}
47+
</div>
48+
);
49+
};
50+
51+
export default UpdatesCard;
Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
import React from 'react';
2+
import BaseStatCard from '../BaseStatCard';
3+
4+
const ActiveMembersCard = ({ count }) => (
5+
<BaseStatCard
6+
title="Active Members"
7+
value={count ?? 0}
8+
/>
9+
);
10+
11+
export default ActiveMembersCard;
Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
import React from 'react';
2+
import BaseStatCard from '../BaseStatCard';
3+
4+
const BudgetCard = ({ budget }) => {
5+
const used = budget?.used ?? 0;
6+
const total = budget?.total ?? 0;
7+
const percentage = total > 0 ? ((used / total) * 100).toFixed(0) : 0;
8+
9+
return (
10+
<BaseStatCard
11+
title="Club Budget Used"
12+
value={`₹${used.toLocaleString()}`}
13+
footer={`${percentage}% of ₹${total.toLocaleString()}`}
14+
/>
15+
);
16+
};
17+
18+
export default BudgetCard;

0 commit comments

Comments
 (0)