Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
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
12 changes: 12 additions & 0 deletions migrations/20260214152404_create_leave_table.sql
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
-- Leave table for tracking leaves
CREATE TABLE Leave (
leave_id SERIAL PRIMARY KEY,
discord_id VARCHAR(255) REFERENCES Member(discord_id) ON DELETE CASCADE,
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 (from_date, discord_id)
);
61 changes: 60 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, 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};
use crate::models::attendance::{AttendanceRecord, MarkAttendanceInput, MarkLeaveOutput};

type HmacSha256 = Hmac<Sha256>;

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

Ok(attendance)
}

#[graphql(name = "leaveApplication", guard = "AdminOrBotGuard")]
async fn leave_application(
&self,
ctx: &Context<'_>,
discord_id: String,
reason: String,
applied_at: NaiveDateTime,
from_date: NaiveDate,
duration: i32,
) -> Result<MarkLeaveOutput> {
let pool = ctx
.data::<Arc<PgPool>>()
.expect("Pool not found in context");

let leave: MarkLeaveOutput = sqlx::query_as::<_, MarkLeaveOutput>(
"INSERT INTO Leave
(discord_id, reason, applied_at, from_date, duration)
VALUES ($1, $2, $3, $4, $5)
RETURNING *
",
)
.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<MarkLeaveOutput> {
let pool = ctx
.data::<Arc<PgPool>>()
.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?;

Ok(leave)
}
}
56 changes: 50 additions & 6 deletions src/graphql/queries/member_queries.rs
Original file line number Diff line number Diff line change
Expand Up @@ -57,31 +57,39 @@ 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()),
}
}

/// 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<Member> {
let auth = ctx.data::<AuthContext>()?;
Expand Down Expand Up @@ -389,4 +397,40 @@ 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>>().expect("Pool must be in context.");

if end_date < start_date {
return Err("end_date must be >= start_date".into());
}
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(
self.discord_id
.as_ref()
.expect("Leave count needs discord_id"),
)
.fetch_one(pool.as_ref())
.await?;
Ok(total.unwrap_or(0))
}
}
10 changes: 10 additions & 0 deletions src/models/attendance.rs
Original file line number Diff line number Diff line change
Expand Up @@ -21,3 +21,13 @@ pub struct MarkAttendanceInput {
pub date: NaiveDate,
pub hmac_signature: String,
}

#[derive(SimpleObject, FromRow)]
pub struct MarkLeaveOutput {
pub discord_id: String,
pub from_date: NaiveDate,
pub applied_at: NaiveDateTime,
pub reason: String,
pub duration: i32,
pub approved_by: Option<String>,
}