Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
19 commits
Select commit Hold shift + click to select a range
67fcbcf
added controllers for fetching attendance token and marking present
prakash2003pramanick Aug 12, 2025
8796d3a
updated attendance controllers
prakash2003pramanick Aug 12, 2025
4258de3
updated attendance controllers
prakash2003pramanick Aug 12, 2025
46358a9
export attendence controller
shing1Sks Aug 12, 2025
c8ffb38
check access admin + color support for absenties
shing1Sks Aug 12, 2025
4075de2
line change support + removed some comments
shing1Sks Aug 13, 2025
9463e83
local changes
prakash2003pramanick Aug 13, 2025
50ef637
pulled upstream main
prakash2003pramanick Aug 13, 2025
0f1f9c4
fixed regTeamMemEmails being null
prakash2003pramanick Aug 13, 2025
dcf47f6
conditional chaining added for safety
shing1Sks Aug 13, 2025
4c3797f
Merge branch 'feature/attendance' of https://github.com/fed-tech/FED-…
shing1Sks Aug 13, 2025
28ee92a
Merge pull request #67 from shing1Sks/feature/attendance
prakash2003pramanick Aug 13, 2025
39f1b17
attendace schema update : markedAt added + getISTDateTime util added
shing1Sks Aug 13, 2025
8f8870e
Merge branch 'fed-tech:feature/attendance' into feature/attendance
shing1Sks Aug 13, 2025
3b3c2cb
time column added for xlsx export
shing1Sks Aug 13, 2025
d4352be
Merge branch 'feature/attendance' of https://github.com/shing1Sks/FED…
shing1Sks Aug 13, 2025
65b6501
datetime fix for xlsx export
shing1Sks Aug 13, 2025
a37af68
Merge pull request #68 from shing1Sks/feature/attendance
prakash2003pramanick Aug 13, 2025
a25fd90
fixundefinednull
hardikguptaofficialgit Aug 14, 2025
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
2 changes: 2 additions & 0 deletions controllers/auth/loginController.js
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,8 @@ const login = expressAsyncHandler(async (req, res, next) => {
// const { email, password } = req.body;

try {

console.log("user ", req.user);
// const user = await prisma.user.findUnique({
// where: { email }
// });
Expand Down
695 changes: 383 additions & 312 deletions controllers/registration/addRegistration.js

Large diffs are not rendered by default.

13 changes: 4 additions & 9 deletions controllers/registration/getTeamDetails.js
Original file line number Diff line number Diff line change
Expand Up @@ -8,11 +8,6 @@ const expressAsyncHandler = require("express-async-handler");
//@access Private (requires authentication)
const getTeamDetails = expressAsyncHandler(async (req, res, next) => {
try {
console.log("=== getTeamDetails called ===");
console.log("Request params:", req.params);
console.log("Request user:", req.user);
console.log("User email:", req.user?.email);

const { formId } = req.params;
const { email } = req.user;

Expand All @@ -37,14 +32,17 @@ const getTeamDetails = expressAsyncHandler(async (req, res, next) => {
}
});

if (!teamRegistration) {
return next(new ApiError(404, "No team registration found for this user in the specified form"));
}

const teamMembers = await prisma.user.findMany({
where: {
email: {
in: teamRegistration.regTeamMemEmails
}
},
select: {
id: true,
name: true,
email: true,
img: true,
Expand All @@ -54,9 +52,6 @@ const getTeamDetails = expressAsyncHandler(async (req, res, next) => {
}
});




// Check if this is a team event
const isTeamEvent = teamRegistration.form.info.participationType === "Team";

Expand Down
295 changes: 295 additions & 0 deletions controllers/registration/markAttendance.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,295 @@
const { ApiError } = require("../../utils/error/ApiError");
const { PrismaClient } = require("@prisma/client");
const { getISTDateTime } = require("../../utils/datetime/getIST");
const prisma = new PrismaClient();
const jwt = require("jsonwebtoken");
const ExcelJS = require("exceljs");
const formatIST = require("../../utils/datetime/formatIST.js");

const getAttendanceCode = async (req, res, next) => {
try {
const { id: formId } = req.params;
const { teamCode } = req.query;
const { id: userId } = req.user;

if (!formId || !userId) {
return next(new ApiError(400, "Form ID or User ID is missing."));
}

let formRegistrationDetails = null;
// Fetching event details from the database
if (teamCode) {
formRegistrationDetails = await prisma.formRegistration.findFirst({
where: {
formId,
teamCode,
},
});

console.log("formRegistrationDetails:", formRegistrationDetails);
} else {
const allFormRegistrationDetails = await prisma.formRegistration.findMany(
{
where: {
formId,
},
}
);

console.log("allFormRegistrationDetails:", allFormRegistrationDetails);

formRegistrationDetails = allFormRegistrationDetails.find(
(registration) =>
registration.value.some((value) => value.user_id === userId)
);

console.log("Filtered formRegistrationDetails:", formRegistrationDetails);

if (!formRegistrationDetails) {
return next(
new ApiError(404, "Form registration not found for the user.")
);
}
formRegistrationDetails = {
...formRegistrationDetails,
};
}

// If no registration details found, return an error
if (!formRegistrationDetails) {
return next(new ApiError(404, "Form registration not found."));
}

const info = formRegistrationDetails.value.find(
(value) => value.user_id === userId
);

// Put in attendance db
const attendanceData = {
formId: formId,
userId: userId,
teamName: formRegistrationDetails.teamName,
teamCode: formRegistrationDetails.teamCode,
info,
};

// Create or update attendance record
const attendanceDetails = await prisma.attendance.upsert({
where: {
formId_userId_teamCode: {
formId: attendanceData.formId,
userId: attendanceData.userId,
teamCode: attendanceData.teamCode,
},
},
create: attendanceData,
update: attendanceData,
});

// Create JWT token for QR code that expires in 20 minutes
const token = jwt.sign(
{
attendanceToken: attendanceDetails.id,
},
process.env.JWT_SECRET,
{ expiresIn: "20m" }
);

// Return JWT token
return res.status(200).json({
message: "Validation id generated successfully.",
attendanceToken: token,
});
} catch (error) {
console.error("Error generating QR link:", error);
return next(
new ApiError(
error.statusCode || 500,
error.message || "Internal Server Error",
error
)
);
}
};

const markAttendance = async (req, res, next) => {
const { formId, token } = req.body;

if (!token) {
return next(new ApiError(400, "Attendance token is required."));
}
try {
// Verify the JWT token
let decoded;
try {
decoded = jwt.verify(token, process.env.JWT_SECRET);
} catch (err) {
return next(new ApiError(401, "Invalid or expired QR."));
}
const attendanceId = decoded.attendanceToken;

if (!attendanceId) {
return next(new ApiError(400, "Attendance ID is missing in the token."));
}

// Check if the attendance ID is valid
const attendanceRecord = await prisma.attendance.findUnique({
where: { id: attendanceId },
});

// If no attendance record found, return an error
if (!attendanceRecord) {
return next(new ApiError(404, "Attendance record not found."));
}

// Check if it belongs to the correct form
if (attendanceRecord.formId !== formId) {
return next(
new ApiError(400, "QR does not belong to the specified form.")
);
}

// Check if the attendance is already marked
if (attendanceRecord?.isPresent) {
return next(new ApiError(400, "Attendance already marked."));
}

// Check if payment is verified
// if (!attendanceRecord?.isPaymentVerified) {
// return next(new ApiError(400, "Payment not verified for this attendance."));
// }

// Mark attendance as present
const updatedAttendance = await prisma.attendance.update({
where: { id: attendanceId },
data: { isPresent: true, markedAt: getISTDateTime() },
});

res.status(200).json({
message: "Attendance marked successfully.",
attendance: updatedAttendance,
});
} catch (error) {
console.error("Error marking attendance:", error);
return next(
new ApiError(
error.statusCode || 500,
error.message || "Internal Server Error",
error
)
);
}
};

const exportAttendance = async (req, res, next) => {
try {
const { id: formId } = req.params;
const { teamCode, format } = req.query;

if (!formId) {
return next(new ApiError(400, "Form ID is required."));
}

const whereClause = { formId };
if (teamCode) whereClause.teamCode = teamCode;

const records = await prisma.attendance.findMany({
where: whereClause,
});

if (!records.length) {
return next(new ApiError(404, "No attendance records found."));
}

// JSON output (unchanged)
if (!format || format === "json") {
let grouped;
if (teamCode) {
grouped = [{ teamCode, members: records }];
} else {
grouped = Object.values(
records.reduce((acc, record) => {
if (!acc[record.teamCode]) {
acc[record.teamCode] = { teamCode: record.teamCode, members: [] };
}
acc[record.teamCode].members.push(record);
return acc;
}, {})
);
}
return res.status(200).json(grouped);
}

// XLSX output with styling
if (format === "xlsx") {
const workbook = new ExcelJS.Workbook();
const sheet = workbook.addWorksheet("Attendance");

// Define columns
sheet.columns = [
{ header: "User ID", key: "userId", width: 25 },
{ header: "Team Name", key: "teamName", width: 20 },
{ header: "Team Code", key: "teamCode", width: 15 },
{ header: "Present", key: "isPresent", width: 10 },
{ header: "Payment Verified", key: "isPaymentVerified", width: 20 },
{ header: "Phone Number", key: "phoneNumber", width: 15 },
{ header: "Marked At (IST)", key: "markedAtIST", width: 25 },
];

// Group by teamCode
const grouped = records.reduce((acc, r) => {
if (!acc[r.teamCode]) acc[r.teamCode] = [];
acc[r.teamCode].push(r);
return acc;
}, {});

Object.keys(grouped).forEach((teamCode) => {
grouped[teamCode].forEach((r) => {
const readableTime = r.markedAt ? formatIST(r.markedAt) : "";

const row = sheet.addRow({
...r,
phoneNumber: "1234",
markedAtIST: readableTime,
});

if (!r.isPresent) {
row.eachCell((cell) => {
cell.fill = {
type: "pattern",
pattern: "solid",
fgColor: { argb: "FFFFCCCC" }, // light red
};
});
}
});

// Add a blank row for separation
sheet.addRow({});
});

// Write buffer
const buffer = await workbook.xlsx.writeBuffer();
res.setHeader(
"Content-Disposition",
`attachment; filename="attendance_${formId}.xlsx"`
);
res.setHeader(
"Content-Type",
"application/vnd.openxmlformats-officedocument.spreadsheetml.sheet"
);
return res.send(buffer);
}

return next(new ApiError(400, "Invalid format. Supported: json, xlsx."));
} catch (error) {
console.error(error);
next(new ApiError(500, "Server error."));
}
};

module.exports = {
getAttendanceCode,
markAttendance,
exportAttendance,
};
20 changes: 14 additions & 6 deletions controllers/registration/registrationController.js
Original file line number Diff line number Diff line change
@@ -1,9 +1,17 @@
const { addRegistration } = require('./addRegistration');
const { getRegistrationCount } = require('./countRegistration');
const { downloadRegistration } = require('./downloadRegistration');
const { addRegistration } = require("./addRegistration");
const { getRegistrationCount } = require("./countRegistration");
const { downloadRegistration } = require("./downloadRegistration");
const {
getAttendanceCode,
markAttendance,
exportAttendance,
} = require("./markAttendance");

module.exports = {
addRegistration,
downloadRegistration,
getRegistrationCount
addRegistration,
downloadRegistration,
getRegistrationCount,
getAttendanceCode,
markAttendance,
exportAttendance,
};
10 changes: 5 additions & 5 deletions index.js
Original file line number Diff line number Diff line change
Expand Up @@ -113,9 +113,9 @@ app.use(express.urlencoded({ extended: true, limit: "16kb" }));
app.use(cookieParser());

app.use(cors({
origin: "*",
methods: ["GET", "POST", "PUT", "DELETE", "OPTIONS"],
credentials: true,
origin: "*",
methods: ["GET", "POST", "PUT", "DELETE", "OPTIONS"],
credentials: true,
}));
app.options('*', cors()); // handle preflight requests

Expand Down Expand Up @@ -182,11 +182,11 @@ app.use(jsonParseErrorHandler);
// Error-handling middleware - should be at the end
app.use(errorHandler);

app.use('/keep-alive',(req,res) => {
app.use('/keep-alive', (req, res) => {
res.sendStatus(200);
// res.status(200).json({message : "Alive"});
})
app.use('/',(req,res) => {
app.use('/', (req, res) => {
res.sendStatus(404);
})
// Start server
Expand Down
Loading