From 1066b877d53f156f79ac75fb7009b1125f8ff669 Mon Sep 17 00:00:00 2001 From: Max Hohlfeld Date: Sun, 26 Jan 2025 22:31:53 +0100 Subject: [PATCH] feat: WIP availability changeset --- ...1163668ad8e7b2153437f803a1f344d16f62b.json | 53 ++++++++ web/src/endpoints/assignment/mod.rs | 2 +- web/src/endpoints/availability/get_new.rs | 36 ++---- .../endpoints/availability/get_overview.rs | 24 ++-- web/src/endpoints/availability/get_update.rs | 46 +++---- web/src/endpoints/availability/mod.rs | 74 +---------- web/src/endpoints/availability/post_new.rs | 19 +-- web/src/endpoints/availability/post_update.rs | 39 ++++-- web/src/endpoints/events/get_plan.rs | 2 +- web/src/endpoints/events/post_edit.rs | 23 +--- .../endpoints/export/get_availability_data.rs | 52 +++++--- web/src/main.rs | 4 + web/src/models/assignment_changeset.rs | 11 +- web/src/models/availability_changeset.rs | 120 +++++++++++++++++- web/src/models/availabillity.rs | 117 ++++++++++++----- web/src/models/mod.rs | 8 +- web/templates/availability/new_or_edit.html | 48 +++---- web/templates/events/plan_personal_table.html | 10 +- web/templates/index.html | 10 +- 19 files changed, 430 insertions(+), 268 deletions(-) create mode 100644 .sqlx/query-5ccea87a5c61aa6ae0fa7a824a61163668ad8e7b2153437f803a1f344d16f62b.json diff --git a/.sqlx/query-5ccea87a5c61aa6ae0fa7a824a61163668ad8e7b2153437f803a1f344d16f62b.json b/.sqlx/query-5ccea87a5c61aa6ae0fa7a824a61163668ad8e7b2153437f803a1f344d16f62b.json new file mode 100644 index 00000000..8df7cfea --- /dev/null +++ b/.sqlx/query-5ccea87a5c61aa6ae0fa7a824a61163668ad8e7b2153437f803a1f344d16f62b.json @@ -0,0 +1,53 @@ +{ + "db_name": "PostgreSQL", + "query": "\n SELECT\n availabillity.id,\n availabillity.userId,\n availabillity.date,\n availabillity.startTime,\n availabillity.endTime,\n availabillity.comment\n FROM availabillity\n WHERE availabillity.userId = $1\n AND availabillity.date = $2;\n ", + "describe": { + "columns": [ + { + "ordinal": 0, + "name": "id", + "type_info": "Int4" + }, + { + "ordinal": 1, + "name": "userid", + "type_info": "Int4" + }, + { + "ordinal": 2, + "name": "date", + "type_info": "Date" + }, + { + "ordinal": 3, + "name": "starttime", + "type_info": "Time" + }, + { + "ordinal": 4, + "name": "endtime", + "type_info": "Time" + }, + { + "ordinal": 5, + "name": "comment", + "type_info": "Text" + } + ], + "parameters": { + "Left": [ + "Int4", + "Date" + ] + }, + "nullable": [ + false, + false, + false, + true, + true, + true + ] + }, + "hash": "5ccea87a5c61aa6ae0fa7a824a61163668ad8e7b2153437f803a1f344d16f62b" +} diff --git a/web/src/endpoints/assignment/mod.rs b/web/src/endpoints/assignment/mod.rs index c370661f..1ecfffe0 100644 --- a/web/src/endpoints/assignment/mod.rs +++ b/web/src/endpoints/assignment/mod.rs @@ -2,7 +2,7 @@ use rinja::Template; use crate::{ filters, - models::{Availability, AvailabillityAssignmentState, Event, Function}, + models::{Availability, AvailabilityTime, AvailabillityAssignmentState, Event, Function}, }; pub mod delete; diff --git a/web/src/endpoints/availability/get_new.rs b/web/src/endpoints/availability/get_new.rs index 04ec1ad9..b5a73dad 100644 --- a/web/src/endpoints/availability/get_new.rs +++ b/web/src/endpoints/availability/get_new.rs @@ -4,8 +4,10 @@ use rinja::Template; use serde::Deserialize; use sqlx::PgPool; -use crate::endpoints::availability::{calc_free_slots_cor, NewOrEditAvailabilityTemplate}; -use crate::models::{Availability, User}; +use crate::endpoints::availability::NewOrEditAvailabilityTemplate; +use crate::models::{ + find_free_time_slots, only_one_availability_exists_and_is_whole_day, Availability, User, +}; use crate::utils::ApplicationError; #[derive(Deserialize)] @@ -21,24 +23,13 @@ pub async fn get( pool: web::Data, query: web::Query, ) -> Result { - let availabillities = Availability::read_by_date_and_area_including_user( - pool.get_ref(), - query.date, - user.area_id, - ) - .await?; + let availabilities_from_user = + Availability::read_by_user_and_date(pool.get_ref(), user.id, &query.date).await?; + let free_slots = find_free_time_slots(&availabilities_from_user); - let availabilities_from_user: Vec<&Availability> = availabillities - .iter() - .filter(|a| a.user_id == user.id) - .collect(); - let only_one_availability_is_wholeday = availabilities_from_user.len() == 1 - && availabilities_from_user[0].start_time.is_none() - && availabilities_from_user[0].end_time.is_none(); - - let user_can_create_availabillity = availabilities_from_user.len() == 0 - || !(only_one_availability_is_wholeday - || calc_free_slots_cor(&availabilities_from_user).len() == 0); + let user_can_create_availabillity = + !(only_one_availability_exists_and_is_whole_day(&availabilities_from_user) + || free_slots.len() == 0); if !user_can_create_availabillity { return Ok(HttpResponse::BadRequest().finish()); @@ -47,12 +38,11 @@ pub async fn get( let template = NewOrEditAvailabilityTemplate { user: user.into_inner(), date: query.date, - whole_day: query.whole_day.unwrap_or(true), + whole_day_selected: query.whole_day.unwrap_or(true), id: None, - start_time: None, - end_time: None, + time: None, comment: None, - slot_suggestions: calc_free_slots_cor(&availabilities_from_user), + slot_suggestions: free_slots, }; Ok(HttpResponse::Ok().body(template.render()?)) diff --git a/web/src/endpoints/availability/get_overview.rs b/web/src/endpoints/availability/get_overview.rs index dc257fcd..d83d383b 100644 --- a/web/src/endpoints/availability/get_overview.rs +++ b/web/src/endpoints/availability/get_overview.rs @@ -1,8 +1,13 @@ use crate::{ - endpoints::availability::calc_free_slots_cor, filters, models::{Assignment, Function, Vehicle}, utils::{event_planning_template::generate_vehicles_assigned_and_available, ApplicationError} + filters, + models::{ + find_free_time_slots, only_one_availability_exists_and_is_whole_day, Assignment, + AvailabilityTime, Function, Vehicle, + }, + utils::{event_planning_template::generate_vehicles_assigned_and_available, ApplicationError}, }; use actix_web::{web, HttpResponse, Responder}; -use chrono::{NaiveDate, NaiveTime, Utc}; +use chrono::{NaiveDate, Utc}; use rinja::Template; use serde::Deserialize; use sqlx::PgPool; @@ -64,17 +69,12 @@ async fn get( ) .await?; - let availabilities_from_user: Vec<&Availability> = availabillities - .iter() - .filter(|a| a.user_id == user.id) - .collect(); - let only_one_availability_is_wholeday = availabilities_from_user.len() == 1 - && availabilities_from_user[0].start_time.is_none() - && availabilities_from_user[0].end_time.is_none(); + let availabilities_from_user = + Availability::read_by_user_and_date(pool.get_ref(), user.id, &date).await?; - let user_can_create_availabillity = availabilities_from_user.len() == 0 - || !(only_one_availability_is_wholeday - || calc_free_slots_cor(&availabilities_from_user).len() == 0); + let user_can_create_availabillity = + !(only_one_availability_exists_and_is_whole_day(&availabilities_from_user) + || find_free_time_slots(&availabilities_from_user).len() == 0); let mut events_and_assignments = Vec::new(); for e in Event::read_all_by_date_and_area_including_location( diff --git a/web/src/endpoints/availability/get_update.rs b/web/src/endpoints/availability/get_update.rs index 0924d270..54d69aab 100644 --- a/web/src/endpoints/availability/get_update.rs +++ b/web/src/endpoints/availability/get_update.rs @@ -5,10 +5,10 @@ use sqlx::PgPool; use crate::{ endpoints::{ - availability::{calc_free_slots_cor, NewOrEditAvailabilityTemplate}, + availability::NewOrEditAvailabilityTemplate, IdPath, }, - models::{Availability, User}, + models::{find_free_time_slots, Availability, AvailabilityTime, User}, utils::ApplicationError, }; @@ -25,42 +25,27 @@ pub async fn get( path: web::Path, query: web::Query, ) -> Result { - let Some(availabillity) = Availability::read_by_id(pool.get_ref(), path.id).await? else { + let Some(availability) = Availability::read_by_id(pool.get_ref(), path.id).await? else { return Ok(HttpResponse::NotFound().finish()); }; - if availabillity.user_id != user.id { + if availability.user_id != user.id { return Err(ApplicationError::Unauthorized); } - let start_time = availabillity - .start_time - .and_then(|d| Some(d.format("%R").to_string())); + let whole_day_selected = query.whole_day.unwrap_or(availability.time == AvailabilityTime::WholeDay); - let end_time = availabillity - .end_time - .and_then(|d| Some(d.format("%R").to_string())); - - let has_time = availabillity.start_time.is_some() && availabillity.end_time.is_some(); - - let suggestions = if has_time { - let availabillities = Availability::read_by_date_and_area_including_user( + let suggestions = if let AvailabilityTime::Temporarily(start, end) = availability.time { + let availabilities = Availability::read_by_user_and_date( pool.get_ref(), - availabillity.date, - user.area_id, + user.id, + &availability.date, ) .await?; - let availabilities_from_user: Vec<&Availability> = availabillities - .iter() - .filter(|a| a.user_id == user.id) - .collect(); - - calc_free_slots_cor(&availabilities_from_user) + find_free_time_slots(&availabilities) .into_iter() - .filter(|(a, b)| { - *b == availabillity.start_time.unwrap() || *a == availabillity.end_time.unwrap() - }) + .filter(|(a, b)| *b == start || *a == end) .collect() } else { Vec::new() @@ -68,13 +53,12 @@ pub async fn get( let template = NewOrEditAvailabilityTemplate { user: user.into_inner(), - date: availabillity.date, - whole_day: query.whole_day.unwrap_or(!has_time), + date: availability.date, id: Some(path.id), - start_time: start_time.as_deref(), - end_time: end_time.as_deref(), - comment: availabillity.comment.as_deref(), + time: Some(availability.time), + comment: availability.comment.as_deref(), slot_suggestions: suggestions, + whole_day_selected }; Ok(HttpResponse::Ok().body(template.render()?)) diff --git a/web/src/endpoints/availability/mod.rs b/web/src/endpoints/availability/mod.rs index d9afa84b..f2d47a06 100644 --- a/web/src/endpoints/availability/mod.rs +++ b/web/src/endpoints/availability/mod.rs @@ -2,7 +2,7 @@ use chrono::{NaiveDate, NaiveTime}; use rinja::Template; use crate::filters; -use crate::models::{Availability, Role, User}; +use crate::models::{AvailabilityTime, Role, User}; pub mod delete; pub mod get_new; @@ -16,77 +16,9 @@ pub mod post_update; struct NewOrEditAvailabilityTemplate<'a> { user: User, date: NaiveDate, - whole_day: bool, id: Option, - start_time: Option<&'a str>, - end_time: Option<&'a str>, + time: Option, + whole_day_selected: bool, comment: Option<&'a str>, slot_suggestions: Vec<(NaiveTime, NaiveTime)>, } - -fn calc_free_slots_cor(availabilities: &Vec<&Availability>) -> Vec<(NaiveTime, NaiveTime)> { - let mut times = Vec::new(); - - for a in availabilities { - let Some(start_time) = a.start_time else { - continue; - }; - - let Some(end_time) = a.end_time else { - continue; - }; - - times.push((start_time, end_time)); - } - - if times.len() == 0 { - return Vec::new(); - } - println!("zeiten {times:?}"); - - times.sort(); - - println!("zeiten sort {times:?}"); - - let mut changed = true; - while changed { - changed = false; - - for i in 0..(times.len() - 1) { - let b = times[i + 1].clone(); - let a = times.get_mut(i).unwrap(); - - if a.1 == b.0 { - a.1 = b.1; - times.remove(i + 1); - changed = true; - break; - } - } - } - - let start = NaiveTime::parse_from_str("00:00", "%R").unwrap(); - let end = NaiveTime::parse_from_str("23:59", "%R").unwrap(); - println!("zeiten unified {times:?}"); - - // now times contains unified list of existing availabilities -> now calculate the "inverse" - - let mut available_slots = Vec::new(); - if times.first().unwrap().0 != start { - available_slots.push((start, times.first().unwrap().0)); - } - - let mut iterator = times.iter().peekable(); - while let Some(a) = iterator.next() { - if let Some(b) = iterator.peek() { - available_slots.push((a.1, b.0)); - } - } - - if times.last().unwrap().1 != end { - available_slots.push((times.last().unwrap().1, end)); - } - - println!("available {available_slots:?}"); - available_slots -} diff --git a/web/src/endpoints/availability/post_new.rs b/web/src/endpoints/availability/post_new.rs index 9eea1b65..2f99bf95 100644 --- a/web/src/endpoints/availability/post_new.rs +++ b/web/src/endpoints/availability/post_new.rs @@ -22,15 +22,16 @@ pub async fn post( pool: web::Data, form: web::Form, ) -> Result { - Availability::create( - pool.get_ref(), - user.id, - form.date, - form.from, - form.till, - form.comment.clone(), - ) - .await?; + // TODO: create and validate changeset + //Availability::create( + // pool.get_ref(), + // user.id, + // form.date, + // form.from, + // form.till, + // form.comment.clone(), + //) + //.await?; let url = utils::get_return_url_for_date(&form.date); Ok(HttpResponse::Found() diff --git a/web/src/endpoints/availability/post_update.rs b/web/src/endpoints/availability/post_update.rs index 64fd27e3..f0fcc290 100644 --- a/web/src/endpoints/availability/post_update.rs +++ b/web/src/endpoints/availability/post_update.rs @@ -1,9 +1,10 @@ use actix_web::{http::header::LOCATION, web, HttpResponse, Responder}; +use garde::Validate; use sqlx::PgPool; use crate::{ endpoints::{availability::post_new::AvailabillityForm, IdPath}, - models::{Availability, User}, + models::{Availability, AvailabilityChangeset, AvailabilityContext, AvailabilityTime, User}, utils::{self, ApplicationError}, }; @@ -22,19 +23,29 @@ pub async fn post( return Err(ApplicationError::Unauthorized); } - if availabillity.start_time != form.from - || availabillity.end_time != form.till - || availabillity.comment != form.comment - { - Availability::update( - pool.get_ref(), - availabillity.id, - form.from, - form.till, - form.comment.as_ref(), - ) - .await?; - } + let existing_availabilities = + Availability::read_by_user_and_date(pool.get_ref(), user.id, &availabillity.date).await?; + let context = AvailabilityContext { + existing_availabilities, + }; + + let time = if form.from.is_some() && form.till.is_some() { + AvailabilityTime::Temporarily(form.from.unwrap(), form.till.unwrap()) + } else { + AvailabilityTime::WholeDay + }; + + let changeset = AvailabilityChangeset { + time, + comment: form.comment.clone(), + }; + + if let Err(e) = changeset.validate_with(&context) { + return Ok(HttpResponse::BadRequest().body(e.to_string())); + }; + + // TODO: check if adjacent availabillity is present and combine + Availability::update(pool.get_ref(), availabillity.id, changeset).await?; let url = utils::get_return_url_for_date(&form.date); Ok(HttpResponse::Found() diff --git a/web/src/endpoints/events/get_plan.rs b/web/src/endpoints/events/get_plan.rs index 9ff8d960..ba177ff3 100644 --- a/web/src/endpoints/events/get_plan.rs +++ b/web/src/endpoints/events/get_plan.rs @@ -5,7 +5,7 @@ use sqlx::PgPool; use crate::{ endpoints::IdPath, filters, - models::{Availability, AvailabillityAssignmentState, Event, Function, Role, User, Vehicle}, + models::{Availability, AvailabillityAssignmentState, AvailabilityTime, Event, Function, Role, User, Vehicle}, utils::{ event_planning_template::{ generate_availabillity_assignment_list, generate_status_whether_staff_is_required, diff --git a/web/src/endpoints/events/post_edit.rs b/web/src/endpoints/events/post_edit.rs index 9d3b5e7b..f9418d74 100644 --- a/web/src/endpoints/events/post_edit.rs +++ b/web/src/endpoints/events/post_edit.rs @@ -7,10 +7,11 @@ use sqlx::PgPool; use crate::{ endpoints::IdPath, models::{ - Assignment, AssignmentChangeset, Availability, Event, EventChangeset, EventContext, - Function, Location, Role, User, + Assignment, AssignmentChangeset, Availability, AvailabilityTime, Event, EventChangeset, + EventContext, Function, Location, Role, User, }, utils::{self, ApplicationError}, + END_OF_DAY, START_OF_DAY, }; #[derive(Deserialize)] @@ -87,10 +88,7 @@ pub async fn post( .await?; if all_assignments.len() == 1 { - if availability.start_time.is_some() && availability.end_time.is_some() { - let start = availability.start_time.unwrap(); - let end = availability.end_time.unwrap(); - + if let AvailabilityTime::Temporarily(start, end) = availability.time { if start > common_time.0 { common_time.0 = start; } @@ -100,17 +98,10 @@ pub async fn post( } } } else { - let mut slots = if availability.start_time.is_some() && availability.end_time.is_some() - { - vec![( - availability.start_time.as_ref().unwrap().clone(), - availability.end_time.as_ref().unwrap().clone(), - )] + let mut slots = if let AvailabilityTime::Temporarily(start, end) = availability.time { + vec![(start, end)] } else { - vec![( - NaiveTime::parse_from_str("00:00", "%R").unwrap(), - NaiveTime::parse_from_str("23:59", "%R").unwrap(), - )] + vec![(START_OF_DAY, END_OF_DAY)] }; for a in all_assignments .iter() diff --git a/web/src/endpoints/export/get_availability_data.rs b/web/src/endpoints/export/get_availability_data.rs index 0f008e4d..65d69da3 100644 --- a/web/src/endpoints/export/get_availability_data.rs +++ b/web/src/endpoints/export/get_availability_data.rs @@ -1,17 +1,23 @@ use actix_identity::Identity; -use actix_web::{http::header::{ContentDisposition, ContentType, CONTENT_DISPOSITION}, web, HttpResponse, Responder}; +use actix_web::{ + http::header::{ContentDisposition, ContentType, CONTENT_DISPOSITION}, + web, HttpResponse, Responder, +}; use chrono::{Months, NaiveDate, NaiveTime, Utc}; use serde::{Deserialize, Serialize}; use sqlx::PgPool; -use crate::models::{Availability, Function, Role, User}; +use crate::{ + models::{Availability, AvailabilityTime, Function, Role, User}, + END_OF_DAY, START_OF_DAY, +}; #[derive(Deserialize)] struct ExportQuery { year: u16, month: u8, area_id: Option, - format: String + format: String, } #[derive(Serialize)] @@ -43,7 +49,8 @@ pub async fn get( // TODO: rerwrite let current_user = User::read_by_id(pool.get_ref(), user.id().unwrap().parse().unwrap()) .await - .unwrap().unwrap(); + .unwrap() + .unwrap(); if current_user.role != Role::Admin && current_user.role != Role::AreaManager { return HttpResponse::Unauthorized().finish(); @@ -70,19 +77,23 @@ pub async fn get( let export_availabillities = availabillities .into_iter() - .map(|a| ExportAvailabillity { - name: a.user.as_ref().unwrap().name.clone(), - area: a.user.as_ref().unwrap().area.as_ref().unwrap().name.clone(), - function: a.user.unwrap().function, - date: a.date, - whole_day: a.start_time.is_none() && a.end_time.is_none(), - start_time: a - .start_time - .unwrap_or(NaiveTime::from_hms_opt(0, 0, 0).unwrap()), - end_time: a - .end_time - .unwrap_or(NaiveTime::from_hms_opt(23, 59, 59).unwrap()), - assigned: false, + .map(|a| { + let (start_time, end_time) = if let AvailabilityTime::Temporarily(start, end) = a.time { + (start, end) + } else { + (START_OF_DAY, END_OF_DAY) + }; + + ExportAvailabillity { + name: a.user.as_ref().unwrap().name.clone(), + area: a.user.as_ref().unwrap().area.as_ref().unwrap().name.clone(), + function: a.user.unwrap().function, + date: a.date, + whole_day: a.time == AvailabilityTime::WholeDay, + start_time, + end_time, + assigned: false, + } }) .collect(); @@ -96,13 +107,16 @@ pub async fn get( let out = match query.format.as_str() { "xml" => quick_xml::se::to_string(&export).unwrap_or(String::new()), "json" => serde_json::to_string(&export).unwrap_or(String::new()), - _ => return HttpResponse::BadRequest().finish() + _ => return HttpResponse::BadRequest().finish(), }; if !out.is_empty() { return HttpResponse::Ok() .content_type(ContentType::xml()) - .insert_header((CONTENT_DISPOSITION, ContentDisposition::attachment(format!("export.{}", query.format)))) + .insert_header(( + CONTENT_DISPOSITION, + ContentDisposition::attachment(format!("export.{}", query.format)), + )) .body(out); } diff --git a/web/src/main.rs b/web/src/main.rs index 28e52ad3..7c6a78b4 100644 --- a/web/src/main.rs +++ b/web/src/main.rs @@ -9,6 +9,7 @@ use actix_web::dev::{ServiceFactory, ServiceRequest, ServiceResponse}; use actix_web::{web, App, HttpServer}; use actix_web_static_files::ResourceFiles; use brass_config::{get_env, load_config, Config}; +use chrono::NaiveTime; use mail::Mailer; use sqlx::postgres::PgPool; use sqlx::{Pool, Postgres}; @@ -28,6 +29,9 @@ mod postgres_session_store; include!(concat!(env!("OUT_DIR"), "/generated.rs")); include!(concat!(env!("OUT_DIR"), "/built.rs")); +pub const START_OF_DAY: NaiveTime = NaiveTime::from_hms_opt(0, 0, 0).unwrap(); +pub const END_OF_DAY: NaiveTime = NaiveTime::from_hms_opt(23, 59, 59).unwrap(); + #[actix_web::main] async fn main() -> anyhow::Result<()> { let env = get_env()?; diff --git a/web/src/models/assignment_changeset.rs b/web/src/models/assignment_changeset.rs index 029baf91..a22fca2e 100644 --- a/web/src/models/assignment_changeset.rs +++ b/web/src/models/assignment_changeset.rs @@ -1,7 +1,9 @@ use chrono::NaiveTime; use garde::Validate; -use super::{start_time_lies_before_end_time, Assignment, Availability, Event, Function}; +use super::{ + start_time_lies_before_end_time, Assignment, Availability, AvailabilityTime, Event, Function, +}; #[derive(Validate)] #[garde(allow_unvalidated)] @@ -33,11 +35,8 @@ fn available_time_fits( value: &(NaiveTime, NaiveTime), context: &AssignmentContext, ) -> garde::Result { - if context.availabillity.start_time.is_some() && context.availabillity.end_time.is_some() { - let start = context.availabillity.start_time.as_ref().unwrap(); - let end = context.availabillity.end_time.as_ref().unwrap(); - - if value.0 < *start || value.1 > *end { + if let AvailabilityTime::Temporarily(start, end) = context.availabillity.time { + if value.0 < start || value.1 > end { return Err(garde::Error::new( "time not made available can't be assigned", )); diff --git a/web/src/models/availability_changeset.rs b/web/src/models/availability_changeset.rs index 9bdda2af..93d33700 100644 --- a/web/src/models/availability_changeset.rs +++ b/web/src/models/availability_changeset.rs @@ -1,3 +1,121 @@ -pub struct AvailabilityChangeset { +use chrono::NaiveTime; +use garde::Validate; +use crate::{END_OF_DAY, START_OF_DAY}; + +use super::{start_time_lies_before_end_time, Availability, AvailabilityTime}; + +#[derive(Validate)] +#[garde(allow_unvalidated)] +#[garde(context(AvailabilityContext))] +pub struct AvailabilityChangeset { + #[garde( + custom(time_is_not_already_made_available), + custom(temporarily_start_time_before_end_time) + )] + pub time: AvailabilityTime, + pub comment: Option, +} + +pub struct AvailabilityContext { + pub existing_availabilities: Vec, +} + +pub fn only_one_availability_exists_and_is_whole_day(a: &[Availability]) -> bool { + a.len() == 1 && a[0].time == AvailabilityTime::WholeDay +} + +fn time_is_not_already_made_available( + value: &AvailabilityTime, + context: &AvailabilityContext, +) -> garde::Result { + if let AvailabilityTime::Temporarily(_, _) = value { + if only_one_availability_exists_and_is_whole_day(&context.existing_availabilities) { + return Err(garde::Error::new("cant create a availability while an other availability for the whole day is already present")); + } + + if find_free_time_slots(&context.existing_availabilities).len() == 0 { + return Err(garde::Error::new( + "cant create a availability as every time slot is already filled", + )); + } + } else { + if context.existing_availabilities.len() > 0 { + return Err(garde::Error::new("cant create a availability for the whole day while an other availability is already present")); + } + } + + Ok(()) +} + +fn temporarily_start_time_before_end_time( + value: &AvailabilityTime, + context: &AvailabilityContext, +) -> garde::Result { + if let AvailabilityTime::Temporarily(start, end) = value { + return start_time_lies_before_end_time(&(*start, *end), context); + } + + Ok(()) +} + +pub fn find_free_time_slots(availabilities: &[Availability]) -> Vec<(NaiveTime, NaiveTime)> { + let mut times = Vec::new(); + + for a in availabilities { + if let AvailabilityTime::Temporarily(start, end) = a.time { + times.push((start, end)); + } + } + + if times.len() == 0 { + return Vec::new(); + } + //println!("zeiten {times:?}"); + + times.sort(); + + //println!("zeiten sort {times:?}"); + + let mut changed = true; + while changed { + changed = false; + + for i in 0..(times.len() - 1) { + let b = times[i + 1].clone(); + let a = times.get_mut(i).unwrap(); + + if a.1 == b.0 { + a.1 = b.1; + times.remove(i + 1); + changed = true; + break; + } + } + } + + //println!("zeiten unified {times:?}"); + + // now times contains unified list of existing availabilities -> now calculate the "inverse" + + let mut available_slots = Vec::new(); + let start = times.first().unwrap(); + if start.0 != START_OF_DAY { + available_slots.push((START_OF_DAY, start.0)); + } + + let mut iterator = times.iter().peekable(); + while let Some(a) = iterator.next() { + if let Some(b) = iterator.peek() { + available_slots.push((a.1, b.0)); + } + } + + let end = times.last().unwrap(); + if end.1 != END_OF_DAY { + available_slots.push((end.1, END_OF_DAY)); + } + + //println!("available {available_slots:?}"); + available_slots } diff --git a/web/src/models/availabillity.rs b/web/src/models/availabillity.rs index 0d38e8ba..eac43982 100644 --- a/web/src/models/availabillity.rs +++ b/web/src/models/availabillity.rs @@ -1,7 +1,7 @@ use chrono::{NaiveDate, NaiveTime}; use sqlx::{query, PgPool}; -use super::{Area, Function, Result, Role, User}; +use super::{Area, AvailabilityChangeset, Function, Result, Role, User}; #[derive(Clone, Debug)] pub struct Availability { @@ -9,20 +9,30 @@ pub struct Availability { pub user_id: i32, pub user: Option, pub date: NaiveDate, - pub start_time: Option, - pub end_time: Option, + pub time: AvailabilityTime, pub comment: Option, } +#[derive(Clone, Debug, PartialEq)] +pub enum AvailabilityTime { + WholeDay, + Temporarily(NaiveTime, NaiveTime), +} + impl Availability { + // TODO: fix db name pub async fn create( pool: &PgPool, user_id: i32, date: NaiveDate, - start_time: Option, - end_time: Option, - comment: Option, + changeset: AvailabilityChangeset, ) -> Result<()> { + let (start, end) = if let AvailabilityTime::Temporarily(s, e) = changeset.time { + (Some(s), Some(e)) + } else { + (None, None) + }; + query!( r#" INSERT INTO availabillity (userId, date, startTime, endTime, comment) @@ -30,9 +40,9 @@ impl Availability { "#, user_id, date, - start_time, - end_time, - comment + start, + end, + changeset.comment ) .execute(pool) .await?; @@ -95,8 +105,10 @@ impl Availability { receive_notifications: r.receivenotifications, }), date: r.date, - start_time: r.starttime, - end_time: r.endtime, + time: match (r.starttime, r.endtime) { + (Some(start), Some(end)) => AvailabilityTime::Temporarily(start, end), + (_, _) => AvailabilityTime::WholeDay, + }, comment: r.comment.clone(), }) .collect(); @@ -104,10 +116,7 @@ impl Availability { Ok(availabillities) } - pub async fn read_by_id_including_user( - pool: &PgPool, - id: i32, - ) -> Result> { + pub async fn read_by_id_including_user(pool: &PgPool, id: i32) -> Result> { let record = query!( r##" SELECT @@ -156,8 +165,10 @@ impl Availability { receive_notifications: r.receivenotifications, }), date: r.date, - start_time: r.starttime, - end_time: r.endtime, + time: match (r.starttime, r.endtime) { + (Some(start), Some(end)) => AvailabilityTime::Temporarily(start, end), + (_, _) => AvailabilityTime::WholeDay, + }, comment: r.comment.clone(), }) }); @@ -176,8 +187,10 @@ impl Availability { user_id: record.userid, user: None, date: record.date, - start_time: record.starttime, - end_time: record.endtime, + time: match (record.starttime, record.endtime) { + (Some(start), Some(end)) => AvailabilityTime::Temporarily(start, end), + (_, _) => AvailabilityTime::WholeDay, + }, comment: record.comment.clone(), }) }); @@ -247,8 +260,10 @@ impl Availability { receive_notifications: r.receivenotifications, }), date: r.date, - start_time: r.starttime, - end_time: r.endtime, + time: match (r.starttime, r.endtime) { + (Some(start), Some(end)) => AvailabilityTime::Temporarily(start, end), + (_, _) => AvailabilityTime::WholeDay, + }, comment: r.comment.clone(), }) .collect(); @@ -256,18 +271,60 @@ impl Availability { Ok(availabillities) } - pub async fn update( + pub async fn read_by_user_and_date( pool: &PgPool, - id: i32, - start_time: Option, - end_time: Option, - comment: Option<&String>, - ) -> Result<()> { + user_id: i32, + date: &NaiveDate, + ) -> Result> { + let records = query!( + r##" + SELECT + availabillity.id, + availabillity.userId, + availabillity.date, + availabillity.startTime, + availabillity.endTime, + availabillity.comment + FROM availabillity + WHERE availabillity.userId = $1 + AND availabillity.date = $2; + "##, + user_id, + date + ) + .fetch_all(pool) + .await?; + + let availabillities = records + .iter() + .map(|r| Availability { + id: r.id, + user_id: r.userid, + user: None, + date: r.date, + time: match (r.starttime, r.endtime) { + (Some(start), Some(end)) => AvailabilityTime::Temporarily(start, end), + (_, _) => AvailabilityTime::WholeDay, + }, + comment: r.comment.clone(), + }) + .collect(); + + Ok(availabillities) + } + + pub async fn update(pool: &PgPool, id: i32, changeset: AvailabilityChangeset) -> Result<()> { + let (start, end) = if let AvailabilityTime::Temporarily(s, e) = changeset.time { + (Some(s), Some(e)) + } else { + (None, None) + }; + query!( "UPDATE availabillity SET startTime = $1, endTime = $2, comment = $3 WHERE id = $4", - start_time, - end_time, - comment, + start, + end, + changeset.comment, id ) .execute(pool) diff --git a/web/src/models/mod.rs b/web/src/models/mod.rs index 3440e23a..13e730f8 100644 --- a/web/src/models/mod.rs +++ b/web/src/models/mod.rs @@ -1,6 +1,7 @@ mod area; mod assignement; mod assignment_changeset; +mod availability_changeset; mod availabillity; mod availabillity_assignment_state; mod event; @@ -13,12 +14,15 @@ mod role; mod user; mod vehicle; mod vehicle_assignement; -mod availability_changeset; pub use area::Area; pub use assignement::Assignment; pub use assignment_changeset::{AssignmentChangeset, AssignmentContext}; -pub use availabillity::Availability; +pub use availability_changeset::{ + find_free_time_slots, only_one_availability_exists_and_is_whole_day, AvailabilityChangeset, + AvailabilityContext, +}; +pub use availabillity::{Availability, AvailabilityTime}; pub use availabillity_assignment_state::AvailabillityAssignmentState; use chrono::NaiveTime; pub use event::Event; diff --git a/web/templates/availability/new_or_edit.html b/web/templates/availability/new_or_edit.html index ccef9a96..198f9601 100644 --- a/web/templates/availability/new_or_edit.html +++ b/web/templates/availability/new_or_edit.html @@ -19,26 +19,23 @@
-
+
+
@@ -49,9 +46,16 @@
+ {% let times = ("00:00".to_string(), "23:59".to_string()) -%} + {% if let Some(times) = time -%} + {% if let AvailabilityTime::Temporarily(start, end) = times -%} + {% let times = (start.to_string(), end.to_string()) -%} + {% endif -%} + {% endif -%}
- + +

noch mögliche Zeiträume:

{% for (s, e) in slot_suggestions %} @@ -60,8 +64,8 @@
- +
diff --git a/web/templates/events/plan_personal_table.html b/web/templates/events/plan_personal_table.html index 9e64f84a..e305371d 100644 --- a/web/templates/events/plan_personal_table.html +++ b/web/templates/events/plan_personal_table.html @@ -27,12 +27,12 @@ {{ u.function|show_tree|safe }} - {% if availabillity.start_time.is_some() && availabillity.end_time.is_some() %} - {{ availabillity.start_time.as_ref().unwrap().format("%R") }} bis {{ - availabillity.end_time.as_ref().unwrap().format("%R") }} - {% else %} + {% match availabillity.time %} + {% when AvailabilityTime::Temporarily(start, end) %} + {{ start.format("%R") }} bis {{ end.format("%R") }} + {% when _ %} ganztägig - {% endif %} + {% endmatch %} {{ availabillity.comment.as_deref().unwrap_or("") }} diff --git a/web/templates/index.html b/web/templates/index.html index ae7f9397..f83ed582 100644 --- a/web/templates/index.html +++ b/web/templates/index.html @@ -220,12 +220,12 @@ {{ u.function|show_tree|safe }} - {% if availabillity.start_time.is_some() && availabillity.end_time.is_some() %} - {{ availabillity.start_time.as_ref().unwrap().format("%R") }} bis {{ - availabillity.end_time.as_ref().unwrap().format("%R") }} - {% else %} + {% match availabillity.time %} + {% when AvailabilityTime::Temporarily(start, end) %} + {{ start.format("%R") }} bis {{ end.format("%R") }} + {% when _ %} ganztägig - {% endif %} + {% endmatch %} {{ availabillity.comment.as_deref().unwrap_or("") }}