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
13 changes: 13 additions & 0 deletions migrations/20260214152404_create_leave_table.sql
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
-- Leave table for tracking leaves
CREATE TABLE Leave (
leave_id SERIAL PRIMARY KEY,
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)
);
64 changes: 63 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,65 @@ 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,
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, reason, from_date, duration)
VALUES ($1, $2, $3, $4)
RETURNING *
",
)
.bind(discord_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)
}
}
55 changes: 50 additions & 5 deletions src/graphql/queries/member_queries.rs
Original file line number Diff line number Diff line change
Expand Up @@ -59,27 +59,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 @@ -419,4 +427,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))
}
}
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 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.
Loading