From 51485742728d150c6845649e0d9d3f6d78b7d955 Mon Sep 17 00:00:00 2001 From: Naveen Date: Sun, 15 Feb 2026 11:14:35 +0530 Subject: [PATCH 1/5] feat: mark leaves mutation --- .../20260214152404_create_leave_table.sql | 11 +++++++ src/graphql/mutations/attendance_mutations.rs | 30 ++++++++++++++++++- src/models/attendance.rs | 17 +++++++++++ 3 files changed, 57 insertions(+), 1 deletion(-) create mode 100644 migrations/20260214152404_create_leave_table.sql diff --git a/migrations/20260214152404_create_leave_table.sql b/migrations/20260214152404_create_leave_table.sql new file mode 100644 index 0000000..9f1b6e4 --- /dev/null +++ b/migrations/20260214152404_create_leave_table.sql @@ -0,0 +1,11 @@ +-- Leave table for tracking leaves +CREATE TABLE Leave ( + leave_id SERIAL PRIMARY KEY, + discord_id VARCHAR(255) REFERENCES Member(discord_id) ON DELETE CASCADE, + date DATE DEFAULT CURRENT_DATE, + duration INT DEFAULt 1, + reason TEXT NOT NULL, + approved_by VARCHAR(255) REFERENCES Member(discord_id), + CHECK (approved_by IS NULL OR approved_by <> discord_id), + UNIQUE (date, discord_id) +); diff --git a/src/graphql/mutations/attendance_mutations.rs b/src/graphql/mutations/attendance_mutations.rs index 953b032..97fc936 100644 --- a/src/graphql/mutations/attendance_mutations.rs +++ b/src/graphql/mutations/attendance_mutations.rs @@ -7,7 +7,7 @@ use sha2::Sha256; use sqlx::PgPool; use crate::auth::guards::AdminOrBotGuard; -use crate::models::attendance::{AttendanceRecord, MarkAttendanceInput}; +use crate::models::attendance::{AttendanceRecord, MarkAttendanceInput, MarkLeaveInput, MarkLeaveOutput}; type HmacSha256 = Hmac; @@ -61,4 +61,32 @@ impl AttendanceMutations { Ok(attendance) } + + async fn mark_leave( + &self, + ctx: &Context<'_>, + input: MarkLeaveInput, + ) -> Result { + let pool = ctx + .data::>() + .expect("Pool not found in context"); + let now = chrono::Utc::now().with_timezone(&Kolkata); + + let leave: MarkLeaveOutput = sqlx::query_as::<_, MarkLeaveOutput>( + "INSERT INTO Leave + (discord_id, date, duration, reason, approved_by) + VALUES ($1, $2, $3, $4, $5) + RETURNING * + ", + ) + .bind(input.discord_id) + .bind(now) + .bind(input.duration) + .bind(input.reason) + .bind(input.approved_by) + .fetch_one(pool.as_ref()) + .await?; + + Ok(leave) + } } diff --git a/src/models/attendance.rs b/src/models/attendance.rs index 8647774..89884c5 100644 --- a/src/models/attendance.rs +++ b/src/models/attendance.rs @@ -21,3 +21,20 @@ pub struct MarkAttendanceInput { pub date: NaiveDate, pub hmac_signature: String, } + +#[derive(InputObject)] +pub struct MarkLeaveInput { + pub discord_id: String, + pub reason: String, + pub duration: i32, + pub approved_by: Option, +} + +#[derive(SimpleObject, FromRow)] +pub struct MarkLeaveOutput { + pub discord_id: String, + pub date: NaiveDate, + pub reason: String, + pub duration: i32, + pub approved_by: Option, +} \ No newline at end of file From f09c779be602e20e39e49041d6ac46c1dd893274 Mon Sep 17 00:00:00 2001 From: Naveen Date: Tue, 17 Feb 2026 18:35:53 +0530 Subject: [PATCH 2/5] add: discord_id param to member queries --- src/graphql/queries/member_queries.rs | 20 ++++++++++++++------ 1 file changed, 14 insertions(+), 6 deletions(-) diff --git a/src/graphql/queries/member_queries.rs b/src/graphql/queries/member_queries.rs index 2b0b1af..00ef38e 100644 --- a/src/graphql/queries/member_queries.rs +++ b/src/graphql/queries/member_queries.rs @@ -57,11 +57,12 @@ impl MemberQueries { ctx: &Context<'_>, member_id: Option, email: Option, + discord_id: Option, ) -> Result> { let pool = ctx.data::>().expect("Pool must be in context."); - match (member_id, email) { - (Some(id), None) => { + match (member_id, email, discord_id) { + (Some(id), None, None) => { let member = sqlx::query_as::<_, Member>("SELECT * FROM Member WHERE member_id = $1") .bind(id) @@ -69,19 +70,26 @@ impl MemberQueries { .await?; Ok(member) } - (None, Some(email)) => { + (None, Some(email), None) => { let member = sqlx::query_as::<_, Member>("SELECT * FROM Member WHERE email = $1") .bind(email) .fetch_optional(pool.as_ref()) .await?; Ok(member) } - (Some(_), Some(_)) => Err("Provide only one of member_id or email".into()), - (None, None) => Err("Provide either member_id or email".into()), + (None, None, Some(discord_id)) => { + let member = + sqlx::query_as::<_, Member>("SELECT * FROM Member WHERE discord_id = $1") + .bind(discord_id) + .fetch_optional(pool.as_ref()) + .await?; + Ok(member) + } + _ => Err("Provide exactly one of member_id, email, or discord_id".into()), } } - /// Fetch the details of the currently logged in member + // Fetch the details of the currently logged in member #[graphql(guard = "AuthGuard")] async fn me(&self, ctx: &Context<'_>) -> Result { let auth = ctx.data::()?; From f7b758b0fc86cecf8dba65944ecb8fc20d046018 Mon Sep 17 00:00:00 2001 From: Naveen Date: Thu, 19 Feb 2026 20:32:31 +0530 Subject: [PATCH 3/5] feat: add query for leave count --- src/graphql/mutations/attendance_mutations.rs | 6 ++- src/graphql/queries/member_queries.rs | 38 ++++++++++++++++++- src/models/attendance.rs | 8 +++- 3 files changed, 48 insertions(+), 4 deletions(-) diff --git a/src/graphql/mutations/attendance_mutations.rs b/src/graphql/mutations/attendance_mutations.rs index 97fc936..52a5601 100644 --- a/src/graphql/mutations/attendance_mutations.rs +++ b/src/graphql/mutations/attendance_mutations.rs @@ -7,7 +7,9 @@ use sha2::Sha256; use sqlx::PgPool; use crate::auth::guards::AdminOrBotGuard; -use crate::models::attendance::{AttendanceRecord, MarkAttendanceInput, MarkLeaveInput, MarkLeaveOutput}; +use crate::models::attendance::{ + AttendanceRecord, MarkAttendanceInput, MarkLeaveInput, MarkLeaveOutput, +}; type HmacSha256 = Hmac; @@ -71,7 +73,7 @@ impl AttendanceMutations { .data::>() .expect("Pool not found in context"); let now = chrono::Utc::now().with_timezone(&Kolkata); - + let leave: MarkLeaveOutput = sqlx::query_as::<_, MarkLeaveOutput>( "INSERT INTO Leave (discord_id, date, duration, reason, approved_by) diff --git a/src/graphql/queries/member_queries.rs b/src/graphql/queries/member_queries.rs index 00ef38e..2840456 100644 --- a/src/graphql/queries/member_queries.rs +++ b/src/graphql/queries/member_queries.rs @@ -1,6 +1,9 @@ use crate::auth::guards::AuthGuard; use crate::auth::AuthContext; -use crate::models::{attendance::AttendanceRecord, status_update::StatusUpdateRecord}; +use crate::models::{ + attendance::{AttendanceRecord, LeaveCountOutput}, + status_update::StatusUpdateRecord, +}; use async_graphql::{ComplexObject, Context, Object, Result}; use chrono::NaiveDate; use sqlx::PgPool; @@ -397,4 +400,37 @@ impl Member { member_id: self.member_id, } } + + async fn leave_count( + &self, + ctx: &Context<'_>, + start_date: NaiveDate, + end_date: NaiveDate, + ) -> Result { + let pool = ctx.data::>().expect("Pool must be in context."); + + if end_date < start_date { + return Err("end_date must be >= start_date".into()); + } + let leave = sqlx::query_as::<_, LeaveCountOutput>( + r#" + SELECT discord_id, SUM(duration) AS count + FROM "Leave" + WHERE date > $1 + AND (date + duration) < $2 + AND discord_id = $3 + GROUP BY discord_id + "#, + ) + .bind(start_date) + .bind(end_date) + .bind( + self.discord_id + .as_ref() + .expect("Leave count needs discord_id"), + ) + .fetch_one(pool.as_ref()) + .await?; + Ok(leave) + } } diff --git a/src/models/attendance.rs b/src/models/attendance.rs index 89884c5..0d3a8b9 100644 --- a/src/models/attendance.rs +++ b/src/models/attendance.rs @@ -37,4 +37,10 @@ pub struct MarkLeaveOutput { pub reason: String, pub duration: i32, pub approved_by: Option, -} \ No newline at end of file +} + +#[derive(SimpleObject, FromRow)] +pub struct LeaveCountOutput { + pub discord_id: String, + pub count: i32, +} From 013cdef7a609f759d0ca5fe61c65d1c93bf03137 Mon Sep 17 00:00:00 2001 From: Naveen Date: Tue, 24 Feb 2026 20:08:36 +0530 Subject: [PATCH 4/5] feat(leave_tracking): apply and approve leave mutations --- .../20260214152404_create_leave_table.sql | 7 +-- src/graphql/mutations/attendance_mutations.rs | 51 +++++++++++++++---- src/models/attendance.rs | 11 +--- 3 files changed, 47 insertions(+), 22 deletions(-) diff --git a/migrations/20260214152404_create_leave_table.sql b/migrations/20260214152404_create_leave_table.sql index 9f1b6e4..9246f67 100644 --- a/migrations/20260214152404_create_leave_table.sql +++ b/migrations/20260214152404_create_leave_table.sql @@ -2,10 +2,11 @@ CREATE TABLE Leave ( leave_id SERIAL PRIMARY KEY, discord_id VARCHAR(255) REFERENCES Member(discord_id) ON DELETE CASCADE, - date DATE DEFAULT CURRENT_DATE, - duration INT DEFAULt 1, + from_date DATE DEFAULT CURRENT_DATE, + duration INT DEFAULT 1, reason TEXT NOT NULL, + applied_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, approved_by VARCHAR(255) REFERENCES Member(discord_id), CHECK (approved_by IS NULL OR approved_by <> discord_id), - UNIQUE (date, discord_id) + UNIQUE (from_date, discord_id) ); diff --git a/src/graphql/mutations/attendance_mutations.rs b/src/graphql/mutations/attendance_mutations.rs index 52a5601..9ef7122 100644 --- a/src/graphql/mutations/attendance_mutations.rs +++ b/src/graphql/mutations/attendance_mutations.rs @@ -1,6 +1,7 @@ use std::sync::Arc; use async_graphql::{Context, Object, Result}; +use chrono::{NaiveDateTime, NaiveDate}; use chrono_tz::Asia::Kolkata; use hmac::{Hmac, Mac}; use sha2::Sha256; @@ -8,7 +9,7 @@ use sqlx::PgPool; use crate::auth::guards::AdminOrBotGuard; use crate::models::attendance::{ - AttendanceRecord, MarkAttendanceInput, MarkLeaveInput, MarkLeaveOutput, + AttendanceRecord, MarkAttendanceInput, MarkLeaveOutput }; type HmacSha256 = Hmac; @@ -64,28 +65,58 @@ impl AttendanceMutations { Ok(attendance) } - async fn mark_leave( + #[graphql(name = "leaveApplication", guard = "AdminOrBotGuard")] + async fn leave_application( &self, ctx: &Context<'_>, - input: MarkLeaveInput, + discord_id: String, + reason: String, + applied_at: NaiveDateTime, + from_date: NaiveDate, + duration: i32 ) -> Result { let pool = ctx .data::>() .expect("Pool not found in context"); - let now = chrono::Utc::now().with_timezone(&Kolkata); let leave: MarkLeaveOutput = sqlx::query_as::<_, MarkLeaveOutput>( "INSERT INTO Leave - (discord_id, date, duration, reason, approved_by) + (discord_id, reason, applied_at, from_date, duration) VALUES ($1, $2, $3, $4, $5) RETURNING * ", ) - .bind(input.discord_id) - .bind(now) - .bind(input.duration) - .bind(input.reason) - .bind(input.approved_by) + .bind(discord_id) + .bind(reason) + .bind(applied_at) + .bind(from_date) + .bind(duration) + .fetch_one(pool.as_ref()) + .await?; + + Ok(leave) + } + + #[graphql(name = "approveLeave", guard = "AdminOrBotGuard")] + async fn approve_leave( + &self, + ctx: &Context<'_>, + discord_id: String, + approved_by: String, + ) -> Result { + let pool = ctx + .data::>() + .expect("Pool not found in context"); + + let leave: MarkLeaveOutput = sqlx::query_as::<_, MarkLeaveOutput>( + "UPDATE Leave + SET approved_by = $1 + WHERE discord_id = $2 + RETURNING * + ", + ) + .bind(approved_by) + .bind(discord_id) .fetch_one(pool.as_ref()) .await?; diff --git a/src/models/attendance.rs b/src/models/attendance.rs index 0d3a8b9..7553875 100644 --- a/src/models/attendance.rs +++ b/src/models/attendance.rs @@ -22,18 +22,11 @@ pub struct MarkAttendanceInput { pub hmac_signature: String, } -#[derive(InputObject)] -pub struct MarkLeaveInput { - pub discord_id: String, - pub reason: String, - pub duration: i32, - pub approved_by: Option, -} - #[derive(SimpleObject, FromRow)] pub struct MarkLeaveOutput { pub discord_id: String, - pub date: NaiveDate, + pub from_date: NaiveDate, + pub applied_at: NaiveDateTime, pub reason: String, pub duration: i32, pub approved_by: Option, From ad8c10d81f980f8ee0884c0cdc3f423d0278f2e2 Mon Sep 17 00:00:00 2001 From: Naveen Date: Tue, 24 Feb 2026 23:02:21 +0530 Subject: [PATCH 5/5] feat(leave_tracking): leave count query --- src/graphql/mutations/attendance_mutations.rs | 8 ++---- src/graphql/queries/member_queries.rs | 28 +++++++++---------- src/models/attendance.rs | 6 ---- 3 files changed, 17 insertions(+), 25 deletions(-) diff --git a/src/graphql/mutations/attendance_mutations.rs b/src/graphql/mutations/attendance_mutations.rs index 9ef7122..e348e23 100644 --- a/src/graphql/mutations/attendance_mutations.rs +++ b/src/graphql/mutations/attendance_mutations.rs @@ -1,16 +1,14 @@ use std::sync::Arc; use async_graphql::{Context, Object, Result}; -use chrono::{NaiveDateTime, NaiveDate}; +use chrono::{NaiveDate, NaiveDateTime}; use chrono_tz::Asia::Kolkata; use hmac::{Hmac, Mac}; use sha2::Sha256; use sqlx::PgPool; use crate::auth::guards::AdminOrBotGuard; -use crate::models::attendance::{ - AttendanceRecord, MarkAttendanceInput, MarkLeaveOutput -}; +use crate::models::attendance::{AttendanceRecord, MarkAttendanceInput, MarkLeaveOutput}; type HmacSha256 = Hmac; @@ -73,7 +71,7 @@ impl AttendanceMutations { reason: String, applied_at: NaiveDateTime, from_date: NaiveDate, - duration: i32 + duration: i32, ) -> Result { let pool = ctx .data::>() diff --git a/src/graphql/queries/member_queries.rs b/src/graphql/queries/member_queries.rs index 2840456..5bf2d1c 100644 --- a/src/graphql/queries/member_queries.rs +++ b/src/graphql/queries/member_queries.rs @@ -1,9 +1,6 @@ use crate::auth::guards::AuthGuard; use crate::auth::AuthContext; -use crate::models::{ - attendance::{AttendanceRecord, LeaveCountOutput}, - status_update::StatusUpdateRecord, -}; +use crate::models::{attendance::AttendanceRecord, status_update::StatusUpdateRecord}; use async_graphql::{ComplexObject, Context, Object, Result}; use chrono::NaiveDate; use sqlx::PgPool; @@ -406,21 +403,24 @@ impl Member { ctx: &Context<'_>, start_date: NaiveDate, end_date: NaiveDate, - ) -> Result { + ) -> Result { let pool = ctx.data::>().expect("Pool must be in context."); if end_date < start_date { return Err("end_date must be >= start_date".into()); } - let leave = sqlx::query_as::<_, LeaveCountOutput>( + let total: Option = sqlx::query_scalar( r#" - SELECT discord_id, SUM(duration) AS count - FROM "Leave" - WHERE date > $1 - AND (date + duration) < $2 - AND discord_id = $3 - GROUP BY discord_id - "#, + SELECT SUM( + LEAST(from_date + duration - 1, $2) + - GREATEST(from_date, $1) + + 1 + ) + FROM leave + WHERE from_date <= $2 + AND (from_date + duration - 1) >= $1 + AND discord_id = $3 + "#, ) .bind(start_date) .bind(end_date) @@ -431,6 +431,6 @@ impl Member { ) .fetch_one(pool.as_ref()) .await?; - Ok(leave) + Ok(total.unwrap_or(0)) } } diff --git a/src/models/attendance.rs b/src/models/attendance.rs index 7553875..4620892 100644 --- a/src/models/attendance.rs +++ b/src/models/attendance.rs @@ -31,9 +31,3 @@ pub struct MarkLeaveOutput { pub duration: i32, pub approved_by: Option, } - -#[derive(SimpleObject, FromRow)] -pub struct LeaveCountOutput { - pub discord_id: String, - pub count: i32, -}