diff --git a/docs/mutations.md b/docs/mutations.md index 8aa9592..8e826da 100644 --- a/docs/mutations.md +++ b/docs/mutations.md @@ -5,6 +5,8 @@ - [editMember](#editmember) - [markAttendance](#markattendance) - [addAttendance](#addattendance) +- [setActiveProject](#setactiveproject) +- [removeActiveProject](#re) --- @@ -141,4 +143,46 @@ date: NaiveDate! timein: NaiveTime! timeout: NaiveTime! isPresent: Boolean! +``` +--- + +### setActiveProject +Set active project for a member. + +#### GraphQL Mutation +```graphql +mutation { + setActiveProject(id:0,projectName:"project_name") { + id + memberId + projectTitle + } +} +``` + +#### Arguments (all required) +```graphql +id: Int! +projectName: String! +``` + +--- + +### removeActiveProject +Remove active project for a member. + +#### GraphQL Mutation +```graphql +mutation { + removeActiveProject(projectId:0) { + id + memberId + projectTitle + } +} +``` + +#### Arguments (all required) +```graphql +projectId: Int! ``` \ No newline at end of file diff --git a/docs/queries.md b/docs/queries.md index e8a871a..5ab0d32 100644 --- a/docs/queries.md +++ b/docs/queries.md @@ -3,6 +3,11 @@ ## Contents - [getMember](#getmember) - [getAttendance](#getattendance) +- [getAttendanceStreak](#getattendancestreak) +- [getAttendanceSummary](#getattendancesummary) +- [getNonWorkingDays](#getnonworkingdays) +- [getProjects](#getprojects) +- [getUpdateStreak](#getupdatestreak) --- @@ -62,3 +67,141 @@ timein: NaiveTime! timeout: NaiveTime! isPresent: Boolean! ``` + +--- + +### getAttendanceStreak +Retrieve attendance streak between date ranges. + +#### GraphQL Query +```graphql +query { + getAttendanceStreak(startDate:"YYYY-MM-DD",endDate:"YYYY-MM-DD"){ + id + memberId + month + streak + } +} +``` + +#### Arguments +- `startDate` (required): A date in the format `YYYY-MM-DD`. +- `endDate` (required): A date in the format `YYYY-MM-DD`. + +#### Fields +```graphql +id: Int! +memberId: Int! +month: NaiveDate! +streak: Int! +``` + +--- + +### getAttendanceSummary +Retrieve attendance summary between date ranges. + +#### GraphQL Query +```graphql +query { + getAttendanceSummary(startDate:"YYYY-MM-DD",endDate:"YYYY-MM-DD") { + maxDays + memberAttendance { + id + presentDays + } + dailyCount { + date + count + } + } +} +``` + +#### Arguments +- `startDate` (required): A date in the format `YYYY-MM-DD`. +- `endDate` (required): A date in the format `YYYY-MM-DD`. + +#### Fields +Type: AttendanceSummary! +```graphql +maxDays: Int! +memberAttendance: [MemberAttendance!]! +dailyCount: [DailyCount!]! +``` + +Type: MemberAttendance! +```graphql +id: Int! +presentDays: Int! +``` + +Type: DailyCount! +```graphql +date: NaiveDate! +count: Int! +``` + +--- + +### getNonWorkingDays +Retrieve Non Working Days from root. + +#### GraphQL Query +```graphql +query { + getNonWorkingDays +} +``` + +#### Fields +```graphql +[NaiveDate!]! +``` + +--- + +### getProjects +Retrieve active project details for all members. + +#### GraphQL Query +```graphql +query { + getProjects { + id + memberId + projectTitle + } +} +``` + +#### Fields +```graphql +id: Int! +memberId: Int! +projectTitle: String +``` + +--- + +### getUpdateStreak +Retrieve Update streaks for all members. + +#### GraphQL Query +```graphql +query { + getUpdateStreak { + id + streak + maxStreak + } +} +``` + +#### Fields +```graphql +id: Int! +streak: Int +maxStreak: Int +``` \ No newline at end of file diff --git a/migrations/20241226065211_add_attendance_strak_and_projects.sql b/migrations/20241226065211_add_attendance_strak_and_projects.sql new file mode 100644 index 0000000..f075eb1 --- /dev/null +++ b/migrations/20241226065211_add_attendance_strak_and_projects.sql @@ -0,0 +1,14 @@ +CREATE TABLE AttendanceStreak ( + id SERIAL PRIMARY KEY, + member_id INT NOT NULL, + month DATE, + streak INT NOT NULL DEFAULT 0, + CONSTRAINT fkey_member FOREIGN KEY (member_id) REFERENCES Member(id) ON DELETE CASCADE +); + +CREATE TABLE ActiveProjects ( + id SERIAL PRIMARY KEY, + member_id INT NOT NULL, + project_title TEXT, + CONSTRAINT fkey_member FOREIGN KEY (member_id) REFERENCES Member(id) ON DELETE CASCADE +); diff --git a/migrations/20241226074423_attendance_streak_constraint.sql b/migrations/20241226074423_attendance_streak_constraint.sql new file mode 100644 index 0000000..54ba0d8 --- /dev/null +++ b/migrations/20241226074423_attendance_streak_constraint.sql @@ -0,0 +1 @@ +ALTER TABLE AttendanceStreak ADD CONSTRAINT unique_member_month UNIQUE (member_id, month); diff --git a/src/attendance/scheduled_task.rs b/src/attendance/scheduled_task.rs index 9dd6636..a49c8a4 100644 --- a/src/attendance/scheduled_task.rs +++ b/src/attendance/scheduled_task.rs @@ -1,4 +1,4 @@ -use chrono::{Local, NaiveTime}; +use chrono::{Datelike, Local, NaiveTime}; use chrono_tz::Asia::Kolkata; use sqlx::PgPool; use std::sync::Arc; @@ -93,8 +93,89 @@ pub async fn scheduled_task(pool: Arc) { Ok(_) => println!("Leaderboard updated."), Err(e) => eprintln!("Failed to update leaderboard: {:?}", e), } + + // Update attendance streak + update_attendance_streak(member.id, pool.as_ref()).await; } } Err(e) => eprintln!("Failed to fetch members: {:?}", e), } } + +// Function to update attendance streak +async fn update_attendance_streak(member_id: i32, pool: &sqlx::PgPool) { + let today = chrono::Local::now().with_timezone(&chrono_tz::Asia::Kolkata).naive_local(); + let yesterday = today.checked_sub_signed(chrono::Duration::hours(12)).unwrap().date(); + + if today.day() == 1 { + let _ = sqlx::query( + r#" + INSERT INTO AttendanceStreak (member_id, month, streak) + VALUES ($1, date_trunc('month', $2::date AT TIME ZONE 'Asia/Kolkata'), 0) + "#, + ) + .bind(member_id) + .bind(today) + .execute(pool) + .await; + println!("Attendance streak created for member ID: {}", member_id); + } + + let present_attendance = sqlx::query_scalar::<_, i64>( + r#" + SELECT COUNT(*) + FROM Attendance + WHERE id = $1 + AND is_present = true + AND date = $2 + "#, + ) + .bind(member_id) + .bind(yesterday) + .fetch_one(pool) + .await; + + match present_attendance { + Ok(1) => { + let existing_streak = sqlx::query_scalar::<_, i32>( + r#" + SELECT streak + FROM AttendanceStreak + WHERE member_id = $1 + AND month = date_trunc('month', $2::date AT TIME ZONE 'Asia/Kolkata') + "#, + ) + .bind(member_id) + .bind(today) + .fetch_optional(pool) + .await; + + match existing_streak { + Ok(Some(streak)) => { + let _ = sqlx::query( + r#" + UPDATE AttendanceStreak + SET streak = $1 + WHERE member_id = $2 + AND month = date_trunc('month', $3::date AT TIME ZONE 'Asia/Kolkata') + "#, + ) + .bind(streak + 1) + .bind(member_id) + .bind(today) + .execute(pool) + .await; + } + Ok(None) => { + println!("No streak found for member ID: {}", member_id); + } + Err(e) => eprintln!("Error checking streak for member ID {}: {:?}", member_id, e), + } + } + Ok(0) => { + println!("Sreak not incremented for member ID: {}", member_id); + } + Ok(_) => eprintln!("Unexpected attendance value for member ID: {}", member_id), + Err(e) => eprintln!("Error checking attendance for member ID {}: {:?}", member_id, e), + } +} diff --git a/src/db/attendance.rs b/src/db/attendance.rs index 5ba3a3f..f03da67 100644 --- a/src/db/attendance.rs +++ b/src/db/attendance.rs @@ -11,3 +11,30 @@ pub struct Attendance { pub timeout: NaiveTime, pub is_present: bool, } + +#[derive(FromRow, SimpleObject)] +pub struct AttendanceStreak { + pub id: i32, + pub member_id: i32, + pub month: NaiveDate, + pub streak: i32, +} + +#[derive(FromRow, SimpleObject)] +pub struct AttendanceSummary { + pub max_days:i64, + pub member_attendance: Vec, + pub daily_count: Vec, +} + +#[derive(FromRow, SimpleObject)] +pub struct MemberAttendance { + pub id:i32, + pub present_days:i64, +} + +#[derive(FromRow, SimpleObject)] +pub struct DailyCount { + pub date: NaiveDate, + pub count: i64, +} \ No newline at end of file diff --git a/src/db/member.rs b/src/db/member.rs index f439e67..ed646a8 100644 --- a/src/db/member.rs +++ b/src/db/member.rs @@ -14,7 +14,15 @@ pub struct Member { pub year: i32, pub macaddress: String, pub discord_id: Option, - pub group_id: i32, + pub group_id: Option, +} + +#[derive(FromRow, SimpleObject)] +pub struct MemberExtended { + pub member: Member, + pub project_title: Option, + pub attendance_count: Option, + pub update_count: Option, } #[derive(FromRow, SimpleObject)] diff --git a/src/db/mod.rs b/src/db/mod.rs index b2c60e1..11cf39a 100644 --- a/src/db/mod.rs +++ b/src/db/mod.rs @@ -1,4 +1,4 @@ pub mod attendance; pub mod leaderboard; pub mod member; - +pub mod projects; diff --git a/src/db/projects.rs b/src/db/projects.rs new file mode 100644 index 0000000..d0f930f --- /dev/null +++ b/src/db/projects.rs @@ -0,0 +1,9 @@ +use sqlx::FromRow; +use async_graphql::SimpleObject; + +#[derive(FromRow, SimpleObject)] +pub struct ActiveProjects { + id: i32, + member_id: i32, + project_title: Option, +} \ No newline at end of file diff --git a/src/graphql/mutations.rs b/src/graphql/mutations.rs index 422cc97..74a0d55 100644 --- a/src/graphql/mutations.rs +++ b/src/graphql/mutations.rs @@ -11,7 +11,7 @@ use sha2::Sha256; type HmacSha256 = Hmac; -use crate::db::{attendance::Attendance, leaderboard::{CodeforcesStats, LeetCodeStats}, member::Member, member::StreakUpdate}; +use crate::db::{attendance::Attendance, leaderboard::{CodeforcesStats, LeetCodeStats}, member::Member, member::StreakUpdate, projects::ActiveProjects}; pub struct MutationRoot; @@ -313,4 +313,49 @@ impl MutationRoot { } } } + + async fn set_active_project( + &self, + ctx: &Context<'_>, + id: i32, + project_name:String, + ) -> Result { + let pool = ctx.data::>().expect("Pool not found in context"); + + let active_project = sqlx::query_as::<_,ActiveProjects>( + " + INSERT INTO ActiveProjects (member_id,project_title) + VALUES ($1,$2) + RETURNING * + " + ) + .bind(id) + .bind(project_name) + .fetch_one(pool.as_ref()) + .await?; + + Ok(active_project) + } + + async fn remove_active_project( + &self, + ctx: &Context<'_>, + project_id: i32, + ) -> Result { + let pool = ctx.data::>().expect("Pool not found in context"); + + let active_project = sqlx::query_as::<_,ActiveProjects>( + " + DELETE FROM ActiveProjects + WHERE id = $1 + RETURNING * + " + ) + .bind(project_id) + .fetch_one(pool.as_ref()) + .await?; + + Ok(active_project) + } + } \ No newline at end of file diff --git a/src/graphql/query.rs b/src/graphql/query.rs index 673919a..65c5d76 100644 --- a/src/graphql/query.rs +++ b/src/graphql/query.rs @@ -1,12 +1,10 @@ use async_graphql::{Context, Object}; use chrono::NaiveDate; +use root::db::{attendance::{AttendanceStreak, AttendanceSummary, DailyCount, MemberAttendance}, projects::ActiveProjects}; use sqlx::PgPool; use std::sync::Arc; - use crate::db::{ - attendance::Attendance, member::StreakUpdate, - leaderboard::{CodeforcesStatsWithName, LeaderboardWithMember, LeetCodeStatsWithName}, - member::Member, + attendance::Attendance, leaderboard::{CodeforcesStatsWithName, LeaderboardWithMember, LeetCodeStatsWithName}, member::{Member, StreakUpdate} }; pub struct QueryRoot; @@ -91,7 +89,8 @@ impl QueryRoot { .expect("Pool not found in context"); let attendance_list = sqlx::query_as::<_, Attendance>( - "SELECT id, date, timein, timeout, is_present FROM Attendance WHERE date = $1", + "SELECT id, date, timein, timeout, is_present + FROM Attendance WHERE date = $1", ) .bind(date) .fetch_all(pool.as_ref()) @@ -105,10 +104,136 @@ impl QueryRoot { ) -> Result { let pool = ctx.data::>().expect("Pool not found in context"); let streak = sqlx::query_as::<_, StreakUpdate>("SELECT * FROM StreakUpdate WHERE id = $1") - .bind(id) + .bind(id) .fetch_one(pool.as_ref()) .await?; Ok(streak) } + + async fn get_update_streak( + &self, + ctx: &Context<'_>, + ) -> Result, sqlx::Error> { + let pool = ctx.data::>().expect("Pool not found in context"); + let streak = sqlx::query_as::<_, StreakUpdate>("SELECT * FROM StreakUpdate") + .fetch_all(pool.as_ref()) + .await?; + + Ok(streak) + } + + async fn get_attendance_streak( + &self, + ctx: &Context<'_>, + start_date: NaiveDate, + end_date: NaiveDate, + ) -> Result, sqlx::Error> { + let pool = ctx.data::>().expect("Pool not found in context"); + let attendance_streak = sqlx::query_as::<_,AttendanceStreak>( + "SELECT * from AttendanceStreak + WHERE month >= $1 AND month < $2 + " + ) + .bind(start_date) + .bind(end_date) + .fetch_all(pool.as_ref()) + .await?; + + Ok(attendance_streak) + } + + async fn get_attendance_summary( + &self, + ctx: &Context<'_>, + start_date: NaiveDate, + end_date: NaiveDate, + ) -> Result { + let pool = ctx.data::>().expect("Pool not found in context"); + let attendance_days = sqlx::query_as::<_, (NaiveDate, i64)>( + "SELECT date, COUNT(*) FROM Attendance + WHERE date >= $1 AND date < $2 + AND is_present = true + GROUP BY date ORDER BY date" + ) + .bind(start_date) + .bind(end_date) + .fetch_all(pool.as_ref()) + .await?; + + let member_attendance = sqlx::query_as::<_, (i32, i64)>( + "SELECT id, COUNT(*) FROM Attendance + WHERE date >= $1 AND date < $2 + AND is_present = true + GROUP BY id" + ) + .bind(start_date) + .bind(end_date) + .fetch_all(pool.as_ref()) + .await?; + + let max_count = sqlx::query_scalar::<_, i64>( + "SELECT COUNT(*) FROM Attendance + WHERE date >= $1 AND date < $2 + AND is_present = true" + ) + .bind(start_date) + .bind(end_date) + .fetch_all(pool.as_ref()) + .await?; + + let daily_count = attendance_days + .into_iter().map(|(date, count)| DailyCount{ + date, count + }) + .collect(); + + let member_attendance = member_attendance + .into_iter().map(|(id, present_days)| MemberAttendance{ + id, present_days + }) + .collect(); + + let summaries: AttendanceSummary = AttendanceSummary{ + max_days: max_count[0], + member_attendance, + daily_count, + }; + + Ok(summaries) + } + + pub async fn get_non_working_days( + &self, + ctx: &Context<'_>, + ) -> Result, sqlx::Error> { + let pool = ctx.data::>().expect("Pool not found in context"); + + let dates = sqlx::query_scalar::<_, NaiveDate>( + "SELECT date + FROM Attendance + GROUP BY date + HAVING BOOL_AND(NOT is_present) + ORDER BY date" + ) + .fetch_all(pool.as_ref()) + .await?; + + Ok(dates) + } + + pub async fn get_projects( + &self, + ctx: &Context<'_>, + ) -> Result, sqlx::Error> { + let pool = ctx.data::>().expect("Pool not found in context"); + + let active_projects = sqlx::query_as::<_, ActiveProjects>( + "SELECT * FROM ActiveProjects" + ) + .fetch_all(pool.as_ref()) + .await?; + + Ok(active_projects) + } }