Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Attendance summary query #65

Open
wants to merge 11 commits into
base: develop
Choose a base branch
from
2 changes: 2 additions & 0 deletions .github/workflows/lint.yml
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,8 @@ jobs:
clippy:
name: Clippy
runs-on: ubuntu-latest
env:
DATABASE_URL: "sqlite::memory:"
steps:
- name: Checkout repository
uses: actions/checkout@v4
Expand Down
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -5,3 +5,4 @@ Secrets*.toml
backups/
.env
*.log
.idea/
49 changes: 49 additions & 0 deletions docs/attendance.md
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,35 @@ struct AttendanceSummary {
}
```

### Daily Count
Total Lab count for each date
```rust
pub struct DailyCount {
pub date: String,
pub count: i64,
}
```

### Member Summary
Total lab members attended in the past 6 months
```rust
pub struct MemberAttendanceSummary {
pub id: i32,
pub name: String,
pub present_days: i64,
}
```

### Attendance Report
Attendance report of the club members
```rust
pub struct AttendanceReport {
pub daily_count: Vec<DailyCount>,
pub member_attendance: Vec<MemberAttendanceSummary>,
pub max_days: i64,
}
```

## Queries

### Get Attendance
Expand Down Expand Up @@ -98,6 +127,26 @@ query {
}
```

### Get Attendance Report
Get Attendance report containing lab count and members attendance report of the past 6 months.
`maxDays returns the count of days when lab was open in the past 6 months`
```graphql
query{
getAttendanceSummary(startDate:"2024-12-20", endDate: "2024-12-27"){
memberAttendance{
name,
presentDays
}
dailyCount{
date,
count
}
maxDays
}
}
```


## Daily Task

The `src/daily_task/daily_task.rs` system automatically updates attendance summaries at midnight.
96 changes: 90 additions & 6 deletions src/graphql/queries/attendance_queries.rs
Original file line number Diff line number Diff line change
@@ -1,13 +1,15 @@
use chrono::NaiveDate;
use std::sync::Arc;

use crate::models::{
attendance::{Attendance, AttendanceWithMember},
member::Member,
};
use async_graphql::{Context, Object, Result};
use chrono::NaiveDate;
use sqlx::PgPool;

use crate::models::attendance::{AttendanceReport, DailyCount, MemberAttendanceSummary};

/// Sub-query for the [`Attendance`] table. The queries are:
/// * attendance - get a specific member's attendance details using their member_id, roll_no or discord_id, or by date for all members.
#[derive(Default)]
Expand Down Expand Up @@ -70,6 +72,88 @@ impl AttendanceQueries {
Ok(attendance_query)
}

async fn get_attendance_summary(
&self,
ctx: &Context<'_>,
start_date: String,
end_date: String,
) -> Result<AttendanceReport> {
let pool = ctx.data::<Arc<PgPool>>().expect("Pool must be in context.");

let start = NaiveDate::parse_from_str(&start_date, "%Y-%m-%d")
.map_err(|_| async_graphql::Error::new("Invalid start_date format. Use YYYY-MM-DD"))?;
let end = NaiveDate::parse_from_str(&end_date, "%Y-%m-%d")
.map_err(|_| async_graphql::Error::new("Invalid end_date format. Use YYYY-MM-DD"))?;
if start > end {
return Err(async_graphql::Error::new(
"startDate cannot be greater than endDate.",
));
}

let daily_count_result = sqlx::query!(
r#"
SELECT
attendace.date,
COUNT(CASE WHEN attendace.is_present = true THEN attendace.member_id END) as total_present
FROM Attendance attendace
WHERE attendace.date BETWEEN $1 AND $2
GROUP BY attendace.date
ORDER BY attendace.date
"#,
start,
end
)
.fetch_all(pool.as_ref())
.await;

let daily_count_rows = daily_count_result?;

let daily_count = daily_count_rows
.into_iter()
.map(|row| DailyCount {
date: row.date.to_string(),
count: row.total_present.unwrap_or(0),
})
.collect();

let member_attendance_query = sqlx::query!(
r#"
SELECT member.member_id as "id!", member.name as "name!",
COUNT(attendance.is_present)::int as "present_days!"
FROM Member member
LEFT JOIN Attendance attendance
ON member.member_id = attendance.member_id
AND attendance.is_present AND attendance.date >= CURRENT_DATE - INTERVAL '6 months'
GROUP BY member.member_id, member.name
ORDER BY member.member_id
"#
)
.fetch_all(pool.as_ref())
.await;

let member_attendance = member_attendance_query?
.into_iter()
.map(|row| MemberAttendanceSummary {
id: row.id,
name: row.name,
present_days: row.present_days as i64,
})
.collect();

let max_days = sqlx::query_scalar::<_, i64>(
"SELECT COUNT(DISTINCT date) FROM Attendance
WHERE date >= CURRENT_DATE - INTERVAL '6 months' AND is_present",
)
.fetch_one(pool.as_ref())
.await?;

Ok(AttendanceReport {
daily_count,
member_attendance,
max_days,
})
}

// Query to get attendance by date
async fn attendance_by_date(
&self,
Expand All @@ -79,11 +163,11 @@ impl AttendanceQueries {
let pool = ctx.data::<Arc<PgPool>>().expect("Pool must be in context.");

let records = sqlx::query_as::<_, AttendanceWithMember>(
"SELECT a.attendance_id, a.member_id, a.date, a.is_present,
a.time_in, a.time_out, m.name, m.year
FROM Attendance a
JOIN Member m ON a.member_id = m.member_id
WHERE a.date = $1",
"SELECT attendance.attendance_id, attendance.member_id, attendance.date, attendance.is_present,
attendance.time_in, attendance.time_out, member.name, member.year
FROM Attendance attendance
JOIN Member member ON attendance.member_id = member.member_id
WHERE attendance.date = $1",
)
.bind(date)
.fetch_all(pool.as_ref())
Expand Down
26 changes: 25 additions & 1 deletion src/models/attendance.rs
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ pub struct Attendance {
pub is_present: bool,
pub time_in: Option<NaiveTime>,
pub time_out: Option<NaiveTime>,

#[graphql(skip)] // Don't expose internal fields/meta-data
pub created_at: NaiveDateTime,
#[graphql(skip)]
Expand Down Expand Up @@ -41,14 +42,37 @@ pub struct AttendanceSummaryInfo {
pub days_attended: i32,
}

/// This struct is used to deserialize the input recieved for mutations on attendance.
/// This struct is used to deserialize the input received for mutations on attendance.
#[derive(InputObject)]
pub struct MarkAttendanceInput {
pub member_id: i32,
pub date: NaiveDate,
pub hmac_signature: String,
}

// This struct is used to get the Lab count of a date
#[derive(SimpleObject)]
pub struct DailyCount {
pub date: String,
pub count: i64,
}

// This struct is used to fetch the attended lab of each member
#[derive(SimpleObject)]
pub struct MemberAttendanceSummary {
pub id: i32,
pub name: String,
pub present_days: i64,
}

// This struct is used for getting the combined Attendance report
#[derive(SimpleObject)]
pub struct AttendanceReport {
pub daily_count: Vec<DailyCount>,
pub member_attendance: Vec<MemberAttendanceSummary>,
pub max_days: i64,
}

/// This struct combines attendance data with member name for queries that need both.
/// It joins the Attendance table with Member to include the member's name.
#[derive(SimpleObject, FromRow)]
Expand Down