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
60 changes: 60 additions & 0 deletions src/commands/apply_leave.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
use crate::{Data, Error};
use chrono::NaiveDate;
use poise::serenity_prelude as serenity;
use serenity::all::{CreateEmbed, CreateMessage};

#[poise::command(slash_command)]
pub async fn apply_leave(
ctx: poise::Context<'_, Data, Error>,
#[description = "Start date (YYYY-MM-DD)"] start_date: NaiveDate,
#[description = "Duration in days"] duration: Option<i32>,
reason: String,
) -> Result<(), Error> {
let discord_id = ctx.author().id.to_string();

let duration = duration.unwrap_or(1);
ctx.data()
.graphql_client
.apply_leave(&discord_id, start_date, duration, reason)
.await?;

let embed = if duration == 1 {
CreateEmbed::new()
.title("📝 Leave Request")
.description(format!(
"**User :** <@{}>\n\
**Start Date :** {}\n\
**Duration :** {} day\n\n\
",
discord_id, start_date, duration
))
} else {
CreateEmbed::new()
.title("📝 Leave Request")
.description(format!(
"**User:** <@{}>\n\
**Start Date:** {}\n\
**Duration:** {} days\n\n\
React with ✅ to approve.",
discord_id, start_date, duration
))
};

let message = ctx
.channel_id()
.send_message(ctx, CreateMessage::new().embed(embed))
.await?;

if duration != 1 {
message
.react(ctx, serenity::ReactionType::Unicode("✅".into()))
.await?;

message
.react(ctx, serenity::ReactionType::Unicode("❌".into()))
.await?;
}
ctx.say("Leave request submitted.").await?;

Ok(())
}
6 changes: 5 additions & 1 deletion src/commands/mod.rs
Original file line number Diff line number Diff line change
@@ -1,6 +1,10 @@
mod apply_leave;
mod set_log_level;
mod summary;

use crate::commands::apply_leave::apply_leave;
use crate::commands::set_log_level::set_log_level;
use crate::commands::summary::member_summary;
use serenity::all::RoleId;
use tracing::{debug, instrument};

Expand Down Expand Up @@ -31,7 +35,7 @@ async fn amdctl(ctx: Context<'_>) -> Result<(), Error> {

/// Returns a vector containg [Poise Commands][`poise::Command`]
pub fn get_commands() -> Vec<poise::Command<Data, Error>> {
let commands = vec![amdctl(), set_log_level()];
let commands = vec![amdctl(), set_log_level(), member_summary(), apply_leave()];
debug!(commands = ?commands.iter().map(|c| &c.name).collect::<Vec<_>>());
commands
}
86 changes: 86 additions & 0 deletions src/commands/summary.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,86 @@
use crate::graphql::models::{LeaveCountRecord, MemberSummary};
use crate::ids::THE_LAB_CHANNEL_ID;
use crate::{Data, Error};
use chrono::{Datelike, Local, NaiveDate};
use poise::serenity_prelude::User;
use serenity::all::{ChannelId, CreateEmbed, CreateMessage};

#[poise::command(slash_command)]
pub async fn member_summary(
ctx: poise::Context<'_, Data, Error>,
#[description = "Mention the member"] member: User,
#[description = "Start Date (YYYY-MM-DD)"] start_date: Option<NaiveDate>,
#[description = "End Date (YYYY-MM-DD)"] end_date: Option<NaiveDate>,
) -> Result<(), Error> {
let discord_id = member.id.get().to_string();

// take present date automatically and give this month's date and summary if both
let (start_date, end_date) = match (start_date, end_date) {
(Some(s), Some(e)) => (s, e),

(None, None) => {
let time = Local::now();
let year = time.year();
let month = time.month();
let end = time.date_naive();

let (target_year, target_month) = if end.day() == 1 {
if month == 1 {
(year - 1, 12)
} else {
(year, month - 1)
}
} else {
(year, month)
};

let start = NaiveDate::from_ymd_opt(target_year, target_month, 1)
.ok_or_else(|| anyhow::anyhow!("Invalid date"))?;

(start, end)
}
_ => {
return Err(
anyhow::anyhow!("Either provide both start and end dates, or none.").into(),
);
}
};

let leaves: LeaveCountRecord = ctx
.data()
.graphql_client
.fetch_leaves(&discord_id, start_date, end_date)
.await?;

let summary: MemberSummary = ctx
.data()
.graphql_client
.fetch_member_summary(&discord_id, start_date, end_date)
.await?;

let embed = CreateEmbed::new()
.title("Member Summary📋")
.description(format!(
"**Report of** <@{}>
• Period: **{} → {}**\n\n\
**Attendance 📊**\n\
• Presence: **{:.1}%**\n\n\
**Updates 📝**\n\
• Consistency: **{:.1}%** \n\n\
**Leave Summary 📄**\n\
• Total Leaves: **{}**",
discord_id,
start_date,
end_date,
summary.present_percent,
summary.updates_percent,
leaves.leave_count
));

let lab_channel_id = ChannelId::new(THE_LAB_CHANNEL_ID);
lab_channel_id
.send_message(ctx.http(), CreateMessage::new().add_embed(embed))
.await?;

Ok(())
}
3 changes: 1 addition & 2 deletions src/config.rs
Original file line number Diff line number Diff line change
Expand Up @@ -47,8 +47,7 @@ impl Default for Config {
owner_id: parse_owner_id_env("OWNER_ID"),
prefix_string: String::from("$"),
root_url: std::env::var("ROOT_URL").expect("ROOT_URL was not found in env"),
api_key: std::env::var("AMD_API_KEY")
.expect("AMD_API_KEY was not found in env"),
api_key: std::env::var("AMD_API_KEY").expect("AMD_API_KEY was not found in env"),
}
}
}
Expand Down
1 change: 1 addition & 0 deletions src/graphql/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ You should have received a copy of the GNU General Public License
along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
pub mod models;
pub mod mutations;
pub mod queries;

use std::sync::Arc;
Expand Down
32 changes: 31 additions & 1 deletion src/graphql/models.rs
Original file line number Diff line number Diff line change
Expand Up @@ -15,8 +15,8 @@ GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
use chrono::{NaiveDate, NaiveDateTime};
use serde::Deserialize;

#[derive(Clone, Debug, Deserialize)]
pub struct StatusOnDate {
#[serde(rename = "isSent")]
Expand Down Expand Up @@ -63,3 +63,33 @@ pub struct AttendanceRecord {
#[serde(rename = "timeIn")]
pub time_in: Option<String>,
}

#[derive(Debug, Deserialize, Clone)]
pub struct MemberSummary {
#[serde(rename = "presentPercent")]
pub present_percent: f32,
#[serde(rename = "updatesPercent")]
pub updates_percent: f32,
}

#[derive(Debug, Deserialize, Clone)]
pub struct LeaveCountRecord {
#[serde(rename = "discordId")]
pub discord_id: String,
#[serde(rename = "leaveCount")]
pub leave_count: i32,
}

#[derive(Debug, Deserialize, Clone)]
pub struct LeaveRecord {
#[serde(rename = "discordId")]
pub discord_id: String,
#[serde(rename = "fromDate")]
pub from_date: NaiveDate,
pub duration: i32,
pub reason: String,
#[serde(rename = "approvedBy")]
pub approved_by: Option<String>,
#[serde(rename = "appliedAt")]
pub applied_at: NaiveDateTime,
}
126 changes: 126 additions & 0 deletions src/graphql/mutations.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,126 @@
use crate::graphql::models::LeaveRecord;
use anyhow::Context;
use serde_json::Value;
use tracing::debug;

use super::GraphQLClient;
use chrono::{Local, NaiveDate};

impl GraphQLClient {
pub async fn apply_leave(
&self,
discord_id: &str,
start_date: NaiveDate,
duration: i32,
reason: String,
) -> anyhow::Result<LeaveRecord> {
let today = Local::now().naive_local();
let query = r#"
mutation($discord_id: String!, $start_date: String!, $duration: Int!, $reason: String, $today: String) {
leaveApplication(
discordId: $discord_id,
fromDate: $start_date,
duration: $duration,
reason: $reason,
appliedAt : $today
) {
discordId,
fromDate,
duration,
reason,
approvedBy,
appliedAt
}
}
"#;

let variables = serde_json::json!({
"discord_id": discord_id,
"start_date": start_date.format("%Y-%m-%d").to_string(),
"duration": duration,
"reason": reason,
"today" : today.format("%Y-%m-%dT%H:%M:%S").to_string()
});

debug!("Sending query {}", query);
debug!("With variables: {:?}", variables);

let response = self
.http()
.post(self.root_url())
.bearer_auth(self.api_key())
.json(&serde_json::json!({
"query": query,
"variables": variables
}))
.send()
.await
.context("Failed to successfully post request")?;

let json: Value = response
.json()
.await
.context("Failed to parse response JSON")?;

let leave_value = json["data"]["leaveApplication"].clone();

let leave: LeaveRecord =
serde_json::from_value(leave_value).context("Failed to deserialize LeaveRecord")?;

Ok(leave)
}

pub async fn approve_leave(
&self,
discord_id: &str,
approved_by: &str,
) -> anyhow::Result<LeaveRecord> {
let query = r#"
mutation($discord_id: String!, $mentor_discord_id : String!) {
approveLeave(
discordId: $discord_id,
approvedBy: $mentor_discord_id,

) {
discordId,
fromDate,
duration,
reason,
approvedBy,
appliedAt
}
}
"#;

let variables = serde_json::json!({
"discord_id": discord_id,
"mentor_discord_id" : approved_by
});

debug!("Sending query {}", query);
debug!("With variables: {:?}", variables);

let response = self
.http()
.post(self.root_url())
.bearer_auth(self.api_key())
.json(&serde_json::json!({
"query": query,
"variables": variables
}))
.send()
.await
.context("Failed to successfully post request")?;

let json: serde_json::Value = response
.json()
.await
.context("Failed to parse response JSON")?;
let leave_value = json["data"]["approveLeave"].clone();

let leave: LeaveRecord =
serde_json::from_value(leave_value).context("Failed to deserialize LeaveRecord")?;

Ok(leave)
}
}
Loading
Loading