Skip to content
14 changes: 14 additions & 0 deletions migrations/20260807145605_create_leaves_table.sql
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
-- Leave table for tracking leaves
CREATE TABLE Leave (
leave_id SERIAL PRIMARY KEY,
message_id INT NOT NULL UNIQUE,
discord_id VARCHAR(255) NOT NULL REFERENCES Member(discord_id) ON DELETE CASCADE,
from_date DATE DEFAULT CURRENT_DATE NOT NULL,
duration INT DEFAULT 1 NOT NULL,
reason TEXT NOT NULL,
applied_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
approved_by VARCHAR(255) REFERENCES Member(discord_id) ON DELETE SET NULL,
CHECK (approved_by IS NULL OR approved_by <> discord_id),
CHECK (duration > 0),
UNIQUE (from_date, discord_id)
);
66 changes: 65 additions & 1 deletion src/graphql/mutations/attendance_mutations.rs
Original file line number Diff line number Diff line change
@@ -1,13 +1,14 @@
use std::sync::Arc;

use async_graphql::{Context, Object, Result};
use chrono::NaiveDate;
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};
use crate::models::attendance::{AttendanceRecord, LeaveRecord, MarkAttendanceInput};

type HmacSha256 = Hmac<Sha256>;

Expand Down Expand Up @@ -61,4 +62,67 @@ impl AttendanceMutations {

Ok(attendance)
}

Comment thread
naveensrinivas282 marked this conversation as resolved.
#[graphql(name = "leaveApplication", guard = "AdminOrBotGuard")]
async fn leave_application(
&self,
ctx: &Context<'_>,
discord_id: String,
message_id: String,
reason: String,
from_date: NaiveDate,
duration: i32,
) -> Result<LeaveRecord> {
let pool = ctx
.data::<Arc<PgPool>>()
.expect("Pool not found in context");

let leave: LeaveRecord = sqlx::query_as::<_, LeaveRecord>(
"INSERT INTO Leave
(discord_id, message_id, reason, from_date, duration)
VALUES ($1, $2, $3, $4, $5)
RETURNING *
",
)
.bind(discord_id)
.bind(message_id)
.bind(reason)
.bind(from_date)
.bind(duration)
.fetch_one(pool.as_ref())
.await?;

Ok(leave)
}

#[graphql(name = "approveLeave", guard = "AdminOrBotGuard")]
async fn approve_leave(
Comment thread
naveensrinivas282 marked this conversation as resolved.
&self,
ctx: &Context<'_>,
discord_id: String,
from_date: NaiveDate,
approved_by: String,
) -> Result<LeaveRecord> {
let pool = ctx
.data::<Arc<PgPool>>()
.expect("Pool not found in context");

let leave: LeaveRecord = sqlx::query_as::<_, LeaveRecord>(
"UPDATE Leave
SET approved_by = $1
WHERE discord_id = $2 AND
from_date=$3 AND
approved_by IS NULL
RETURNING *
",
)
.bind(approved_by)
.bind(discord_id)
.bind(from_date)
.fetch_optional(pool.as_ref())
.await?
.ok_or_else(|| async_graphql::Error::new("no pending leave found to approve"))?;

Ok(leave)
}
}
87 changes: 81 additions & 6 deletions src/graphql/queries/member_queries.rs
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,9 @@ use chrono::NaiveDate;
use sqlx::PgPool;
use std::sync::Arc;

use crate::models::{member::Member, status_update::StatusUpdateStreakRecord};
use crate::models::{
attendance::CheckLeave, member::Member, status_update::StatusUpdateStreakRecord,
};

#[derive(Default)]
pub struct MemberQueries;
Expand Down Expand Up @@ -59,27 +61,35 @@ impl MemberQueries {
ctx: &Context<'_>,
member_id: Option<i32>,
email: Option<String>,
discord_id: Option<String>,
) -> Result<Option<Member>> {
let pool = ctx.data::<Arc<PgPool>>().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)
.fetch_optional(pool.as_ref())
.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()),
}
}

Expand Down Expand Up @@ -119,6 +129,34 @@ impl MemberQueries {
})
}
}

#[graphql(guard = "AuthGuard")]
async fn leave_by_message_id(
&self,
ctx: &Context<'_>,
#[graphql(name = "messageId")] message_id: i64,
) -> Result<CheckLeave> {
let pool = ctx.data::<Arc<PgPool>>()?;

let row: Option<(i64, i64, NaiveDate, i64, NaiveDate, Option<String>)> = sqlx::query_as(
"SELECT message_id, discord_id, from_date, duration, applied_at, approved_by FROM Leave WHERE message_id = $1",
)
.bind(message_id)
.fetch_optional(pool.as_ref())
.await?;

match row {
Some((msg_id, discord_id, from_date, duration, applied_at, approved_by)) => Ok(CheckLeave {
message_id: msg_id,
discord_id,
from_date,
duration,
applied_at,
approved_by: approved_by.unwrap_or_else(|| "bot".to_string()),
}),
None => Err("No leave found for given message_id".into()),
}
}
}

#[Object]
Expand Down Expand Up @@ -419,4 +457,41 @@ impl Member {
member_id: self.member_id,
}
}

async fn leave_count(
&self,
ctx: &Context<'_>,
start_date: NaiveDate,
end_date: NaiveDate,
) -> Result<i64> {
let pool = ctx.data::<Arc<PgPool>>()?;

if end_date < start_date {
return Err("end_date must be >= start_date".into());
}
let discord_id = self
.discord_id
.as_ref()
.expect("Leave count needs discord_id");
Comment thread
naveensrinivas282 marked this conversation as resolved.

let total: Option<i64> = sqlx::query_scalar(
r#"
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)
.bind(discord_id)
.fetch_one(pool.as_ref())
.await?;
Ok(total.unwrap_or(0))
}
}
20 changes: 20 additions & 0 deletions src/models/attendance.rs
Original file line number Diff line number Diff line change
Expand Up @@ -21,3 +21,23 @@ pub struct MarkAttendanceInput {
pub date: NaiveDate,
pub hmac_signature: String,
}

#[derive(SimpleObject, FromRow)]
pub struct LeaveRecord {
pub discord_id: String,
pub from_date: NaiveDate,
pub applied_at: NaiveDateTime,
pub reason: String,
pub duration: i32,
pub approved_by: Option<String>,
}
Comment thread
naveensrinivas282 marked this conversation as resolved.

#[derive(SimpleObject)]
pub struct CheckLeave {
pub message_id: i64,
pub discord_id: i64,
pub from_date: NaiveDate,
pub duration: i64,
pub applied_at: NaiveDate,
pub approved_by: String,
}
Loading