Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Feature/vs2961/attendance dashboard #106

Merged
merged 14 commits into from
Jun 18, 2024
2 changes: 1 addition & 1 deletion backend/src/controllers/program.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ export type Program = {
daysOfWeek: string[];
color: string; //colorValueHex;
hourlyPay: string;
sessions: [string[]];
sessions: { start_time: string; end_time: string }[];
archived: boolean;
};

Expand Down
308 changes: 308 additions & 0 deletions backend/src/controllers/session.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,308 @@
import { RequestHandler } from "express";
import { validationResult } from "express-validator";
import mongoose from "mongoose";

import AbsenceSessionModel from "../models/absenceSession";
import EnrollmentModel from "../models/enrollment";
import ProgramModel from "../models/program";
import SessionModel from "../models/session";
import validationErrorParser from "../util/validationErrorParser";

type StudentInfo = {
studentId: mongoose.Types.ObjectId;
attended: boolean;
hoursAttended: number;
};

type Program = {
_id: mongoose.Types.ObjectId;
name: string;
abbreviation: string;
type: string;
daysOfWeek: string[];
color: string; //colorValueHex;
hourlyPay: number;
sessions: { start_time: string; end_time: string }[];
};

export type UpdateSessionBody = {
_id: string;
programId: string;
sessionTime: { start_time: string; end_time: string };
students: StudentInfo[];
};

export type SessionBody = {
programId: mongoose.Types.ObjectId;
sessionTime: { start_time: string; end_time: string };
students: StudentInfo[];
marked: boolean;
date: Date;
isAbsenceSession: boolean;
};

export type AbsenceCreateBody = {
programId: string;
date: Date;
student: StudentInfo;
};

// Gets the dates for the given days of the week since the start date
function getSessionsSince(start: Date, daysOfWeek: string[]): Date[] {
function setToMidnight(date: Date): Date {
return new Date(date.getFullYear(), date.getMonth(), date.getDate());
}

if (start.getFullYear() === new Date(0).getFullYear()) {
const futureDate = setToMidnight(new Date());
while (true) {
const dayOfWeek = futureDate.getUTCDay();
const abbreviatedDay = ["Su", "M", "T", "W", "Th", "F", "Sa"][dayOfWeek];
if (daysOfWeek.includes(abbreviatedDay)) {
return [futureDate];
}
futureDate.setDate(futureDate.getDate() + 1);
}
}

const datesBetween: Date[] = [];
const currentDate = setToMidnight(new Date(start));
while (currentDate <= new Date()) {
const dayOfWeek = currentDate.getUTCDay();
const abbreviatedDay = ["Su", "M", "T", "W", "Th", "F", "Sa"][dayOfWeek];
if (currentDate.getDate() !== start.getDate() && daysOfWeek.includes(abbreviatedDay)) {
datesBetween.push(new Date(currentDate));
}
currentDate.setDate(currentDate.getDate() + 1);
}

return datesBetween;
}

// Gets the number of hours for a particular session (rounded up)
const hoursAttended = (start_time: string, end_time: string) => {
const [startHour, startMinute] = start_time.split(":").map(Number);
const [endHour, endMinute] = end_time.split(":").map(Number);
return endHour - startHour + (endMinute - startMinute >= 30 ? 1 : 0);
};

// Dynamically creates any regular sessions since the last created session, or today
const createMissingRegularSessions = async () => {
const programs = await ProgramModel.find({ type: "regular" }).lean().exec();
const programPromises = programs.map(async (program: Program) => {
// Find the most recent session for the given program
const mostRecentSession = await SessionModel.findOne({
programId: program._id,
isAbsenceSession: false,
})
.sort({ date: -1 })
.exec();

// Get all dates since the last session
let dates;
if (mostRecentSession !== undefined && mostRecentSession !== null) {
dates = getSessionsSince(mostRecentSession.date, program.daysOfWeek);
} else {
dates = getSessionsSince(new Date(0), program.daysOfWeek);
}
const sessionPromises = dates.map(async (date) => {
const dayOfWeek = ["Su", "M", "T", "W", "Th", "F", "Sa"][date.getUTCDay()];
const newSessions = await Promise.all(
program.sessions.map(async (session) => {
// Get all students who are enrolled in this particular session
const enrollments = await EnrollmentModel.find({
"sessionTime.start_time": session.start_time,
"sessionTime.end_time": session.end_time,
schedule: dayOfWeek,
startDate: { $lte: new Date(date) },
programId: program._id,
});
if (enrollments.length === 0) {
return null;
}

// Create default values for the new session
const studentsInfo: StudentInfo[] = enrollments.map((enrollment) => ({
studentId: enrollment.studentId,
attended: true,
hoursAttended: hoursAttended(session.start_time, session.end_time),
}));
const newSession: SessionBody = {
programId: program._id,
sessionTime: session,
students: studentsInfo,
marked: false,
isAbsenceSession: false,
date,
};

return newSession;
}),
);
return SessionModel.create(newSessions.filter((session) => session !== null));
});
await Promise.all(sessionPromises);
});
await Promise.all(programPromises);
};

// Dynamically creates any varying sessions since the last created session, or today
const createMissingVaryingSessions = async () => {
// Get all varying program
const programs = await ProgramModel.find({ type: "varying" }).lean().exec();
const programPromises = programs.map(async (program: Program) => {
// Find most recent session and generate from there
const mostRecentSession = await SessionModel.findOne({
programId: program._id,
isAbsenceSession: false,
})
.sort({ date: -1 })
.exec();

let mostRecentDate = new Date();
if (mostRecentSession !== undefined && mostRecentSession !== null) {
mostRecentDate = mostRecentSession.date;
}

// Get all enrollments belonging to this varying program
const enrollments = await EnrollmentModel.find({
programId: program._id,
startDate: { $lte: mostRecentDate },
renewalDate: { $gte: mostRecentDate },
})
.lean()
.exec();
if (enrollments.length === 0) {
return;
}

const allDays = new Set<string>();
enrollments.forEach((enrollment) => {
enrollment.schedule.forEach((day: string) => allDays.add(day));
});

const daysOfWeek: string[] = Array.from(allDays);

if (mostRecentSession === undefined || mostRecentSession === null) {
mostRecentDate = new Date(0);
}

const dates = getSessionsSince(mostRecentDate, daysOfWeek);

const newSessions = dates.map((date: Date) => {
// Create default values for the new session
const dayOfWeek = ["Su", "M", "T", "W", "Th", "F", "Sa"][date.getUTCDay()];

const enrolledStudents = enrollments.filter((enrollment) =>
enrollment.schedule.includes(dayOfWeek),
);

const studentsInfo: StudentInfo[] = enrolledStudents.map((enrollment) => ({
studentId: enrollment.studentId,
attended: true,
hoursAttended: 0,
}));

const newSession: SessionBody = {
programId: program._id,
students: studentsInfo,
marked: false,
date,
sessionTime: { start_time: "00:00", end_time: "00:00" },
isAbsenceSession: false,
};

return newSession;
});
return SessionModel.create(newSessions);
});
return await Promise.all(programPromises);
};

// Call when creating a session from absence
export const createAbsenceSession: RequestHandler = async (req, res, next) => {

Check warning on line 223 in backend/src/controllers/session.ts

View workflow job for this annotation

GitHub Actions / Backend lint and style check

Promise-returning function provided to variable where a void return was expected
const errors = validationResult(req);

try {
validationErrorParser(errors);

const sessionData = req.body as AbsenceCreateBody;
const programForm = await SessionModel.findOneAndUpdate(
{
date: sessionData.date,
programId: sessionData.programId,
},
{ $push: { students: sessionData.student } },
{ upsert: true, new: true },
);
await AbsenceSessionModel.findOneAndDelete({
programId: sessionData.programId,
studentId: sessionData.student.studentId,
});

res.status(201).json(programForm);
} catch (error) {
next(error);
}
};

// Call when attendance is marked
export const updateSession: RequestHandler = async (req, res, next) => {

Check warning on line 250 in backend/src/controllers/session.ts

View workflow job for this annotation

GitHub Actions / Backend lint and style check

Promise-returning function provided to variable where a void return was expected
const errors = validationResult(req);
try {
validationErrorParser(errors);

const programData = req.body as UpdateSessionBody;

const editedProgram = await SessionModel.findOneAndUpdate(
{ _id: programData._id },
programData,
{
new: true,
},
);

if (!editedProgram) {
return res.status(404).json({ message: "No object in database with provided ID" });
}

const absentStudents = programData.students.filter((student: StudentInfo) => !student.attended);

const absenceSessions = absentStudents.map((absentStudent) => ({
programId: programData.programId,
studentId: absentStudent.studentId,
}));

await AbsenceSessionModel.create(absenceSessions);

res.status(200).json(editedProgram);
} catch (error) {
next(error);
}
};

// Call when frontpage to load recent sessions is called
export const getRecentSessions: RequestHandler = async (_, res, next) => {

Check warning on line 285 in backend/src/controllers/session.ts

View workflow job for this annotation

GitHub Actions / Backend lint and style check

Promise-returning function provided to variable where a void return was expected
try {
await createMissingRegularSessions();
await createMissingVaryingSessions();
// Show in terms of pacific time
const currTime = new Date(new Date().getTime() - 7 * 60 * 60 * 1000);
const sessions = await SessionModel.find({ marked: false, date: { $lte: currTime } });

res.status(200).json(sessions);
} catch (error) {
next(error);
}
};

// Call when looking to populate absence cards
export const getAbsenceSessions: RequestHandler = async (_, res, next) => {

Check warning on line 300 in backend/src/controllers/session.ts

View workflow job for this annotation

GitHub Actions / Backend lint and style check

Promise-returning function provided to variable where a void return was expected
try {
const sessions = await AbsenceSessionModel.find();

res.status(200).json(sessions);
} catch (error) {
next(error);
}
};
13 changes: 13 additions & 0 deletions backend/src/models/absenceSession.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
/**
* Absence session schema
*/
import { InferSchemaType, Schema, model } from "mongoose";

const sessionSchema = new Schema({
programId: { type: Schema.Types.ObjectId, ref: "Program", required: true },
studentId: { type: Schema.Types.ObjectId, ref: "Student", required: true },
});

type AbsenceSession = InferSchemaType<typeof sessionSchema>;

export default model<AbsenceSession>("AbsenceSession", sessionSchema);
8 changes: 7 additions & 1 deletion backend/src/models/enrollment.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,13 @@ const enrollmentSchema = new mongoose.Schema({
dateUpdated: { type: Date, required: true, default: Date.now() },
hoursLeft: { type: Number, required: true },
schedule: { type: [String], required: true },
sessionTime: { type: [String], required: true },
sessionTime: {
type: {
start_time: { type: String, required: true },
end_time: { type: String, required: true },
},
required: true,
},
startDate: { type: Date, required: true },
renewalDate: { type: Date, required: true },
authNumber: { type: String, required: true },
Expand Down
10 changes: 9 additions & 1 deletion backend/src/models/program.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,15 @@ const programSchema = new Schema({
daysOfWeek: { type: [String], required: true }, // M, T, W, TH, F
color: { type: String, required: true },
hourlyPay: { type: Number, required: true },
sessions: { type: [[String]], required: true },
sessions: {
type: [
{
start_time: { type: String, required: true },
end_time: { type: String, required: true },
},
],
required: true,
},
archived: { type: Boolean, required: true },

dateUpdated: { type: String, required: true },
Expand Down
31 changes: 31 additions & 0 deletions backend/src/models/session.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
/**
* Session schema
*/
import { InferSchemaType, Schema, model } from "mongoose";

const sessionSchema = new Schema({
programId: { type: Schema.Types.ObjectId, ref: "Program", required: true },
date: { type: Date, required: true },
sessionTime: {
type: {
start_time: { type: String, required: true },
end_time: { type: String, required: true },
},
},
students: {
type: [
{
studentId: { type: Schema.Types.ObjectId, ref: "Student", required: true },
attended: { type: Boolean, required: true },
hoursAttended: { type: Number, required: true },
},
],
required: true,
},
marked: { type: Boolean, required: true },
isAbsenceSession: { type: Boolean, required: true },
});

type Session = InferSchemaType<typeof sessionSchema>;

export default model<Session>("Session", sessionSchema);
Loading
Loading