From 83ff00d582d1e6e0f53578474f80b2f656eb97c2 Mon Sep 17 00:00:00 2001 From: Chewing_Bever Date: Wed, 17 May 2023 10:52:33 +0200 Subject: [PATCH] feat: add reservation booking --- affluences-api/src/models/available.rs | 9 ++ src/commands/affluence.rs | 70 ----------- src/commands/bib.rs | 158 +++++++++++++++++++++++++ src/commands/mod.rs | 4 +- 4 files changed, 169 insertions(+), 72 deletions(-) delete mode 100644 src/commands/affluence.rs create mode 100644 src/commands/bib.rs diff --git a/affluences-api/src/models/available.rs b/affluences-api/src/models/available.rs index 29df5c5..9042c50 100644 --- a/affluences-api/src/models/available.rs +++ b/affluences-api/src/models/available.rs @@ -73,4 +73,13 @@ impl Resource { .filter(|(hour, _)| hour.state == 1) .collect() } + + /// Returns whether a slot with the given state and time bounds is present in the list of + /// hours. + pub fn has_slot(&self, start_time: NaiveTime, end_time: NaiveTime, state: u32) -> bool { + self.condensed_hours() + .into_iter() + .filter(|(block, _)| block.state == state) + .any(|(block, duration)| start_time >= block.hour && end_time <= block.hour + duration) + } } diff --git a/src/commands/affluence.rs b/src/commands/affluence.rs deleted file mode 100644 index 47d5f9b..0000000 --- a/src/commands/affluence.rs +++ /dev/null @@ -1,70 +0,0 @@ -use crate::commands::EmbedField; -use crate::{Context, Error}; - -use affluences_api::Resource; -use chrono::NaiveDate; -use uuid::{uuid, Uuid}; - -const STERRE_BIB_ID: Uuid = uuid!("4737e57a-ee05-4f7b-901a-7bb541eeb297"); -const TIME_FORMAT: &str = "%H:%M"; - -fn resource_to_embed_field(resource: Resource) -> EmbedField { - let available_hours = resource.condensed_available_hours(); - - if available_hours.is_empty() { - ( - resource.resource_name.clone(), - "Nothing available.".to_string(), - false, - ) - } else { - ( - resource.resource_name.clone(), - available_hours - .into_iter() - .map(|(start_block, duration)| { - format!( - "{} - {} ({:02}:{:02})", - start_block.hour.format(TIME_FORMAT), - (start_block.hour + duration).format(TIME_FORMAT), - duration.num_hours(), - duration.num_minutes() % 60 - ) - }) - .collect::>() - .join("\n"), - false, - ) - } -} - -/// List available timeslots for day -#[poise::command(prefix_command, slash_command)] -pub async fn available(ctx: Context<'_>, date: NaiveDate) -> Result<(), Error> { - let client = &ctx.data().client; - let resources = client.available(STERRE_BIB_ID, date, 1).await?; - - ctx.send(|f| { - f.embed(|e| { - e.description(format!("Available booking dates for {}.", date)) - .fields( - resources - .into_iter() - .map(resource_to_embed_field) - .collect::>(), - ) - }) - }) - .await?; - - Ok(()) -} - -// Create a reservation -// #[poise::command(prefix_command, slash_command)] -// pub async fn reserve( -// ctx: Context<'_>, -// date: NaiveDate, -// ) -> Result<(), Error> { - -// } diff --git a/src/commands/bib.rs b/src/commands/bib.rs new file mode 100644 index 0000000..244c623 --- /dev/null +++ b/src/commands/bib.rs @@ -0,0 +1,158 @@ +use crate::commands::EmbedField; +use crate::db::users::User; +use crate::{Context, Error}; + +use affluences_api::{Reservation, Resource}; +use chrono::{NaiveDate, NaiveTime}; +use uuid::{uuid, Uuid}; + +const STERRE_BIB_ID: Uuid = uuid!("4737e57a-ee05-4f7b-901a-7bb541eeb297"); +const TIME_FORMAT: &str = "%H:%M"; + +/// Parent command for all bib-related commands +/// +/// Commands for reservating study rooms in the bib. +#[poise::command(prefix_command, slash_command, subcommands("available", "book"))] +pub async fn bib(_ctx: Context<'_>) -> Result<(), Error> { + Ok(()) +} + +fn resource_to_embed_field(resource: Resource) -> EmbedField { + let available_hours = resource.condensed_available_hours(); + let title = format!("{} ({}p)", resource.resource_name, resource.capacity); + + if available_hours.is_empty() { + (title, "Nothing available.".to_string(), false) + } else { + ( + title, + available_hours + .into_iter() + .map(|(start_block, duration)| { + format!( + "{} - {} ({:02}:{:02})", + start_block.hour.format(TIME_FORMAT), + (start_block.hour + duration).format(TIME_FORMAT), + duration.num_hours(), + duration.num_minutes() % 60 + ) + }) + .collect::>() + .join("\n"), + false, + ) + } +} + +/// List available timeslots for day +#[poise::command(prefix_command, slash_command)] +pub async fn available(ctx: Context<'_>, date: NaiveDate) -> Result<(), Error> { + let client = &ctx.data().client; + let mut resources = client.available(STERRE_BIB_ID, date, 1).await?; + // Cloning here isn't super efficient, but this list only consists of a handful of elements so + // it's fine + resources.sort_by_key(|k| k.resource_name.clone()); + + ctx.send(|f| { + f.embed(|e| { + e.description(format!("Available booking dates for {}.", date)) + .fields( + resources + .into_iter() + .map(resource_to_embed_field) + .collect::>(), + ) + }) + }) + .await?; + + Ok(()) +} + +#[poise::command(prefix_command, slash_command)] +pub async fn book( + ctx: Context<'_>, + date: NaiveDate, + start_time: NaiveTime, + end_time: NaiveTime, + #[description = "Minimum seats the room should have."] capacity: Option, +) -> Result<(), Error> { + if ctx.guild_id().is_none() { + ctx.say("You have to send this message from a guild.") + .await?; + + return Ok(()); + } + + let guild_id = ctx.guild_id().unwrap(); + let discord_id = ctx.author().id.0 as i64; + + let user = { + let mut conn = ctx.data().pool.get()?; + User::get(&mut conn, guild_id.into(), discord_id)? + }; + + if user.is_none() { + ctx.say("You have to register before being able to book reservations.") + .await?; + + return Ok(()); + } + + let user = user.unwrap(); + + let client = &ctx.data().client; + let resources = client.available(STERRE_BIB_ID, date, 1).await?; + let chosen_resource = resources + .iter() + .filter(|r| capacity.is_none() || capacity.unwrap() <= r.capacity) + .find(|r| r.has_slot(start_time, end_time, 1)); + + if let Some(chosen_resource) = chosen_resource { + let reservation = Reservation { + auth_type: None, + email: user.email.clone(), + date, + start_time, + end_time, + note: "coworking space".to_string(), + user_firstname: user.first_name, + user_lastname: user.last_name, + user_phone: None, + person_count: capacity.unwrap_or(1), + }; + + client + .make_reservation(chosen_resource.resource_id, &reservation) + .await?; + + ctx.send(|f| { + f.embed(|e| { + e.description("A new reservation has been made.") + .field("when", format!("{} {} - {}", date, start_time.format(TIME_FORMAT), end_time.format(TIME_FORMAT)), false) + .field("where", &chosen_resource.resource_name, false) + .footer(|ft| ft.text( + format!("A confirmation mail has been sent to {}. Please check your email and confirm your reservation within two hours.", user.email))) + }) + }) + .await?; + } else { + ctx.say("No slot is available within your requested bounds.") + .await?; + } + + // let resources = if let Some(capacity) = capacity { + // resources.filter(|r| r.capacity >= capacity) + // }; + + Ok(()) +} + +// Create a reservation +// #[poise::command(prefix_command, slash_command)] +// pub async fn reserve( +// ctx: Context<'_>, +// date: NaiveDate, +// ) -> Result<(), Error> { + +// } diff --git a/src/commands/mod.rs b/src/commands/mod.rs index 40068c5..d32fb16 100644 --- a/src/commands/mod.rs +++ b/src/commands/mod.rs @@ -1,4 +1,4 @@ -mod affluence; +mod bib; mod minecraft; mod users; @@ -9,7 +9,7 @@ type EmbedField = (String, String, bool); pub fn commands() -> Vec> { vec![ help(), - affluence::available(), + bib::bib(), minecraft::mc(), users::register(), users::registered(),