feat: WIP availability changeset

This commit is contained in:
Max Hohlfeld 2025-01-26 22:31:53 +01:00
parent 09821c4b8d
commit 1066b877d5
19 changed files with 430 additions and 268 deletions

View File

@ -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"
}

View File

@ -2,7 +2,7 @@ use rinja::Template;
use crate::{ use crate::{
filters, filters,
models::{Availability, AvailabillityAssignmentState, Event, Function}, models::{Availability, AvailabilityTime, AvailabillityAssignmentState, Event, Function},
}; };
pub mod delete; pub mod delete;

View File

@ -4,8 +4,10 @@ use rinja::Template;
use serde::Deserialize; use serde::Deserialize;
use sqlx::PgPool; use sqlx::PgPool;
use crate::endpoints::availability::{calc_free_slots_cor, NewOrEditAvailabilityTemplate}; use crate::endpoints::availability::NewOrEditAvailabilityTemplate;
use crate::models::{Availability, User}; use crate::models::{
find_free_time_slots, only_one_availability_exists_and_is_whole_day, Availability, User,
};
use crate::utils::ApplicationError; use crate::utils::ApplicationError;
#[derive(Deserialize)] #[derive(Deserialize)]
@ -21,24 +23,13 @@ pub async fn get(
pool: web::Data<PgPool>, pool: web::Data<PgPool>,
query: web::Query<AvailabilityNewQuery>, query: web::Query<AvailabilityNewQuery>,
) -> Result<impl Responder, ApplicationError> { ) -> Result<impl Responder, ApplicationError> {
let availabillities = Availability::read_by_date_and_area_including_user( let availabilities_from_user =
pool.get_ref(), Availability::read_by_user_and_date(pool.get_ref(), user.id, &query.date).await?;
query.date, let free_slots = find_free_time_slots(&availabilities_from_user);
user.area_id,
)
.await?;
let availabilities_from_user: Vec<&Availability> = availabillities let user_can_create_availabillity =
.iter() !(only_one_availability_exists_and_is_whole_day(&availabilities_from_user)
.filter(|a| a.user_id == user.id) || free_slots.len() == 0);
.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);
if !user_can_create_availabillity { if !user_can_create_availabillity {
return Ok(HttpResponse::BadRequest().finish()); return Ok(HttpResponse::BadRequest().finish());
@ -47,12 +38,11 @@ pub async fn get(
let template = NewOrEditAvailabilityTemplate { let template = NewOrEditAvailabilityTemplate {
user: user.into_inner(), user: user.into_inner(),
date: query.date, date: query.date,
whole_day: query.whole_day.unwrap_or(true), whole_day_selected: query.whole_day.unwrap_or(true),
id: None, id: None,
start_time: None, time: None,
end_time: None,
comment: None, comment: None,
slot_suggestions: calc_free_slots_cor(&availabilities_from_user), slot_suggestions: free_slots,
}; };
Ok(HttpResponse::Ok().body(template.render()?)) Ok(HttpResponse::Ok().body(template.render()?))

View File

@ -1,8 +1,13 @@
use crate::{ 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 actix_web::{web, HttpResponse, Responder};
use chrono::{NaiveDate, NaiveTime, Utc}; use chrono::{NaiveDate, Utc};
use rinja::Template; use rinja::Template;
use serde::Deserialize; use serde::Deserialize;
use sqlx::PgPool; use sqlx::PgPool;
@ -64,17 +69,12 @@ async fn get(
) )
.await?; .await?;
let availabilities_from_user: Vec<&Availability> = availabillities let availabilities_from_user =
.iter() Availability::read_by_user_and_date(pool.get_ref(), user.id, &date).await?;
.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 let user_can_create_availabillity =
|| !(only_one_availability_is_wholeday !(only_one_availability_exists_and_is_whole_day(&availabilities_from_user)
|| calc_free_slots_cor(&availabilities_from_user).len() == 0); || find_free_time_slots(&availabilities_from_user).len() == 0);
let mut events_and_assignments = Vec::new(); let mut events_and_assignments = Vec::new();
for e in Event::read_all_by_date_and_area_including_location( for e in Event::read_all_by_date_and_area_including_location(

View File

@ -5,10 +5,10 @@ use sqlx::PgPool;
use crate::{ use crate::{
endpoints::{ endpoints::{
availability::{calc_free_slots_cor, NewOrEditAvailabilityTemplate}, availability::NewOrEditAvailabilityTemplate,
IdPath, IdPath,
}, },
models::{Availability, User}, models::{find_free_time_slots, Availability, AvailabilityTime, User},
utils::ApplicationError, utils::ApplicationError,
}; };
@ -25,42 +25,27 @@ pub async fn get(
path: web::Path<IdPath>, path: web::Path<IdPath>,
query: web::Query<EditAvailabilityQuery>, query: web::Query<EditAvailabilityQuery>,
) -> Result<impl Responder, ApplicationError> { ) -> Result<impl Responder, ApplicationError> {
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()); return Ok(HttpResponse::NotFound().finish());
}; };
if availabillity.user_id != user.id { if availability.user_id != user.id {
return Err(ApplicationError::Unauthorized); return Err(ApplicationError::Unauthorized);
} }
let start_time = availabillity let whole_day_selected = query.whole_day.unwrap_or(availability.time == AvailabilityTime::WholeDay);
.start_time
.and_then(|d| Some(d.format("%R").to_string()));
let end_time = availabillity let suggestions = if let AvailabilityTime::Temporarily(start, end) = availability.time {
.end_time let availabilities = Availability::read_by_user_and_date(
.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(
pool.get_ref(), pool.get_ref(),
availabillity.date, user.id,
user.area_id, &availability.date,
) )
.await?; .await?;
let availabilities_from_user: Vec<&Availability> = availabillities find_free_time_slots(&availabilities)
.iter()
.filter(|a| a.user_id == user.id)
.collect();
calc_free_slots_cor(&availabilities_from_user)
.into_iter() .into_iter()
.filter(|(a, b)| { .filter(|(a, b)| *b == start || *a == end)
*b == availabillity.start_time.unwrap() || *a == availabillity.end_time.unwrap()
})
.collect() .collect()
} else { } else {
Vec::new() Vec::new()
@ -68,13 +53,12 @@ pub async fn get(
let template = NewOrEditAvailabilityTemplate { let template = NewOrEditAvailabilityTemplate {
user: user.into_inner(), user: user.into_inner(),
date: availabillity.date, date: availability.date,
whole_day: query.whole_day.unwrap_or(!has_time),
id: Some(path.id), id: Some(path.id),
start_time: start_time.as_deref(), time: Some(availability.time),
end_time: end_time.as_deref(), comment: availability.comment.as_deref(),
comment: availabillity.comment.as_deref(),
slot_suggestions: suggestions, slot_suggestions: suggestions,
whole_day_selected
}; };
Ok(HttpResponse::Ok().body(template.render()?)) Ok(HttpResponse::Ok().body(template.render()?))

View File

@ -2,7 +2,7 @@ use chrono::{NaiveDate, NaiveTime};
use rinja::Template; use rinja::Template;
use crate::filters; use crate::filters;
use crate::models::{Availability, Role, User}; use crate::models::{AvailabilityTime, Role, User};
pub mod delete; pub mod delete;
pub mod get_new; pub mod get_new;
@ -16,77 +16,9 @@ pub mod post_update;
struct NewOrEditAvailabilityTemplate<'a> { struct NewOrEditAvailabilityTemplate<'a> {
user: User, user: User,
date: NaiveDate, date: NaiveDate,
whole_day: bool,
id: Option<i32>, id: Option<i32>,
start_time: Option<&'a str>, time: Option<AvailabilityTime>,
end_time: Option<&'a str>, whole_day_selected: bool,
comment: Option<&'a str>, comment: Option<&'a str>,
slot_suggestions: Vec<(NaiveTime, NaiveTime)>, 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
}

View File

@ -22,15 +22,16 @@ pub async fn post(
pool: web::Data<PgPool>, pool: web::Data<PgPool>,
form: web::Form<AvailabillityForm>, form: web::Form<AvailabillityForm>,
) -> Result<impl Responder, ApplicationError> { ) -> Result<impl Responder, ApplicationError> {
Availability::create( // TODO: create and validate changeset
pool.get_ref(), //Availability::create(
user.id, // pool.get_ref(),
form.date, // user.id,
form.from, // form.date,
form.till, // form.from,
form.comment.clone(), // form.till,
) // form.comment.clone(),
.await?; //)
//.await?;
let url = utils::get_return_url_for_date(&form.date); let url = utils::get_return_url_for_date(&form.date);
Ok(HttpResponse::Found() Ok(HttpResponse::Found()

View File

@ -1,9 +1,10 @@
use actix_web::{http::header::LOCATION, web, HttpResponse, Responder}; use actix_web::{http::header::LOCATION, web, HttpResponse, Responder};
use garde::Validate;
use sqlx::PgPool; use sqlx::PgPool;
use crate::{ use crate::{
endpoints::{availability::post_new::AvailabillityForm, IdPath}, endpoints::{availability::post_new::AvailabillityForm, IdPath},
models::{Availability, User}, models::{Availability, AvailabilityChangeset, AvailabilityContext, AvailabilityTime, User},
utils::{self, ApplicationError}, utils::{self, ApplicationError},
}; };
@ -22,19 +23,29 @@ pub async fn post(
return Err(ApplicationError::Unauthorized); return Err(ApplicationError::Unauthorized);
} }
if availabillity.start_time != form.from let existing_availabilities =
|| availabillity.end_time != form.till Availability::read_by_user_and_date(pool.get_ref(), user.id, &availabillity.date).await?;
|| availabillity.comment != form.comment let context = AvailabilityContext {
{ existing_availabilities,
Availability::update( };
pool.get_ref(),
availabillity.id, let time = if form.from.is_some() && form.till.is_some() {
form.from, AvailabilityTime::Temporarily(form.from.unwrap(), form.till.unwrap())
form.till, } else {
form.comment.as_ref(), AvailabilityTime::WholeDay
) };
.await?;
} 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); let url = utils::get_return_url_for_date(&form.date);
Ok(HttpResponse::Found() Ok(HttpResponse::Found()

View File

@ -5,7 +5,7 @@ use sqlx::PgPool;
use crate::{ use crate::{
endpoints::IdPath, endpoints::IdPath,
filters, filters,
models::{Availability, AvailabillityAssignmentState, Event, Function, Role, User, Vehicle}, models::{Availability, AvailabillityAssignmentState, AvailabilityTime, Event, Function, Role, User, Vehicle},
utils::{ utils::{
event_planning_template::{ event_planning_template::{
generate_availabillity_assignment_list, generate_status_whether_staff_is_required, generate_availabillity_assignment_list, generate_status_whether_staff_is_required,

View File

@ -7,10 +7,11 @@ use sqlx::PgPool;
use crate::{ use crate::{
endpoints::IdPath, endpoints::IdPath,
models::{ models::{
Assignment, AssignmentChangeset, Availability, Event, EventChangeset, EventContext, Assignment, AssignmentChangeset, Availability, AvailabilityTime, Event, EventChangeset,
Function, Location, Role, User, EventContext, Function, Location, Role, User,
}, },
utils::{self, ApplicationError}, utils::{self, ApplicationError},
END_OF_DAY, START_OF_DAY,
}; };
#[derive(Deserialize)] #[derive(Deserialize)]
@ -87,10 +88,7 @@ pub async fn post(
.await?; .await?;
if all_assignments.len() == 1 { if all_assignments.len() == 1 {
if availability.start_time.is_some() && availability.end_time.is_some() { if let AvailabilityTime::Temporarily(start, end) = availability.time {
let start = availability.start_time.unwrap();
let end = availability.end_time.unwrap();
if start > common_time.0 { if start > common_time.0 {
common_time.0 = start; common_time.0 = start;
} }
@ -100,17 +98,10 @@ pub async fn post(
} }
} }
} else { } else {
let mut slots = if availability.start_time.is_some() && availability.end_time.is_some() let mut slots = if let AvailabilityTime::Temporarily(start, end) = availability.time {
{ vec![(start, end)]
vec![(
availability.start_time.as_ref().unwrap().clone(),
availability.end_time.as_ref().unwrap().clone(),
)]
} else { } else {
vec![( vec![(START_OF_DAY, END_OF_DAY)]
NaiveTime::parse_from_str("00:00", "%R").unwrap(),
NaiveTime::parse_from_str("23:59", "%R").unwrap(),
)]
}; };
for a in all_assignments for a in all_assignments
.iter() .iter()

View File

@ -1,17 +1,23 @@
use actix_identity::Identity; 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 chrono::{Months, NaiveDate, NaiveTime, Utc};
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
use sqlx::PgPool; 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)] #[derive(Deserialize)]
struct ExportQuery { struct ExportQuery {
year: u16, year: u16,
month: u8, month: u8,
area_id: Option<i32>, area_id: Option<i32>,
format: String format: String,
} }
#[derive(Serialize)] #[derive(Serialize)]
@ -43,7 +49,8 @@ pub async fn get(
// TODO: rerwrite // TODO: rerwrite
let current_user = User::read_by_id(pool.get_ref(), user.id().unwrap().parse().unwrap()) let current_user = User::read_by_id(pool.get_ref(), user.id().unwrap().parse().unwrap())
.await .await
.unwrap().unwrap(); .unwrap()
.unwrap();
if current_user.role != Role::Admin && current_user.role != Role::AreaManager { if current_user.role != Role::Admin && current_user.role != Role::AreaManager {
return HttpResponse::Unauthorized().finish(); return HttpResponse::Unauthorized().finish();
@ -70,19 +77,23 @@ pub async fn get(
let export_availabillities = availabillities let export_availabillities = availabillities
.into_iter() .into_iter()
.map(|a| ExportAvailabillity { .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(), name: a.user.as_ref().unwrap().name.clone(),
area: a.user.as_ref().unwrap().area.as_ref().unwrap().name.clone(), area: a.user.as_ref().unwrap().area.as_ref().unwrap().name.clone(),
function: a.user.unwrap().function, function: a.user.unwrap().function,
date: a.date, date: a.date,
whole_day: a.start_time.is_none() && a.end_time.is_none(), whole_day: a.time == AvailabilityTime::WholeDay,
start_time: a start_time,
.start_time end_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, assigned: false,
}
}) })
.collect(); .collect();
@ -96,13 +107,16 @@ pub async fn get(
let out = match query.format.as_str() { let out = match query.format.as_str() {
"xml" => quick_xml::se::to_string(&export).unwrap_or(String::new()), "xml" => quick_xml::se::to_string(&export).unwrap_or(String::new()),
"json" => serde_json::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() { if !out.is_empty() {
return HttpResponse::Ok() return HttpResponse::Ok()
.content_type(ContentType::xml()) .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); .body(out);
} }

View File

@ -9,6 +9,7 @@ use actix_web::dev::{ServiceFactory, ServiceRequest, ServiceResponse};
use actix_web::{web, App, HttpServer}; use actix_web::{web, App, HttpServer};
use actix_web_static_files::ResourceFiles; use actix_web_static_files::ResourceFiles;
use brass_config::{get_env, load_config, Config}; use brass_config::{get_env, load_config, Config};
use chrono::NaiveTime;
use mail::Mailer; use mail::Mailer;
use sqlx::postgres::PgPool; use sqlx::postgres::PgPool;
use sqlx::{Pool, Postgres}; use sqlx::{Pool, Postgres};
@ -28,6 +29,9 @@ mod postgres_session_store;
include!(concat!(env!("OUT_DIR"), "/generated.rs")); include!(concat!(env!("OUT_DIR"), "/generated.rs"));
include!(concat!(env!("OUT_DIR"), "/built.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] #[actix_web::main]
async fn main() -> anyhow::Result<()> { async fn main() -> anyhow::Result<()> {
let env = get_env()?; let env = get_env()?;

View File

@ -1,7 +1,9 @@
use chrono::NaiveTime; use chrono::NaiveTime;
use garde::Validate; 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)] #[derive(Validate)]
#[garde(allow_unvalidated)] #[garde(allow_unvalidated)]
@ -33,11 +35,8 @@ fn available_time_fits(
value: &(NaiveTime, NaiveTime), value: &(NaiveTime, NaiveTime),
context: &AssignmentContext, context: &AssignmentContext,
) -> garde::Result { ) -> garde::Result {
if context.availabillity.start_time.is_some() && context.availabillity.end_time.is_some() { if let AvailabilityTime::Temporarily(start, end) = context.availabillity.time {
let start = context.availabillity.start_time.as_ref().unwrap(); if value.0 < start || value.1 > end {
let end = context.availabillity.end_time.as_ref().unwrap();
if value.0 < *start || value.1 > *end {
return Err(garde::Error::new( return Err(garde::Error::new(
"time not made available can't be assigned", "time not made available can't be assigned",
)); ));

View File

@ -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<String>,
}
pub struct AvailabilityContext {
pub existing_availabilities: Vec<Availability>,
}
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
} }

View File

@ -1,7 +1,7 @@
use chrono::{NaiveDate, NaiveTime}; use chrono::{NaiveDate, NaiveTime};
use sqlx::{query, PgPool}; use sqlx::{query, PgPool};
use super::{Area, Function, Result, Role, User}; use super::{Area, AvailabilityChangeset, Function, Result, Role, User};
#[derive(Clone, Debug)] #[derive(Clone, Debug)]
pub struct Availability { pub struct Availability {
@ -9,20 +9,30 @@ pub struct Availability {
pub user_id: i32, pub user_id: i32,
pub user: Option<User>, pub user: Option<User>,
pub date: NaiveDate, pub date: NaiveDate,
pub start_time: Option<NaiveTime>, pub time: AvailabilityTime,
pub end_time: Option<NaiveTime>,
pub comment: Option<String>, pub comment: Option<String>,
} }
#[derive(Clone, Debug, PartialEq)]
pub enum AvailabilityTime {
WholeDay,
Temporarily(NaiveTime, NaiveTime),
}
impl Availability { impl Availability {
// TODO: fix db name
pub async fn create( pub async fn create(
pool: &PgPool, pool: &PgPool,
user_id: i32, user_id: i32,
date: NaiveDate, date: NaiveDate,
start_time: Option<NaiveTime>, changeset: AvailabilityChangeset,
end_time: Option<NaiveTime>,
comment: Option<String>,
) -> Result<()> { ) -> Result<()> {
let (start, end) = if let AvailabilityTime::Temporarily(s, e) = changeset.time {
(Some(s), Some(e))
} else {
(None, None)
};
query!( query!(
r#" r#"
INSERT INTO availabillity (userId, date, startTime, endTime, comment) INSERT INTO availabillity (userId, date, startTime, endTime, comment)
@ -30,9 +40,9 @@ impl Availability {
"#, "#,
user_id, user_id,
date, date,
start_time, start,
end_time, end,
comment changeset.comment
) )
.execute(pool) .execute(pool)
.await?; .await?;
@ -95,8 +105,10 @@ impl Availability {
receive_notifications: r.receivenotifications, receive_notifications: r.receivenotifications,
}), }),
date: r.date, date: r.date,
start_time: r.starttime, time: match (r.starttime, r.endtime) {
end_time: r.endtime, (Some(start), Some(end)) => AvailabilityTime::Temporarily(start, end),
(_, _) => AvailabilityTime::WholeDay,
},
comment: r.comment.clone(), comment: r.comment.clone(),
}) })
.collect(); .collect();
@ -104,10 +116,7 @@ impl Availability {
Ok(availabillities) Ok(availabillities)
} }
pub async fn read_by_id_including_user( pub async fn read_by_id_including_user(pool: &PgPool, id: i32) -> Result<Option<Availability>> {
pool: &PgPool,
id: i32,
) -> Result<Option<Availability>> {
let record = query!( let record = query!(
r##" r##"
SELECT SELECT
@ -156,8 +165,10 @@ impl Availability {
receive_notifications: r.receivenotifications, receive_notifications: r.receivenotifications,
}), }),
date: r.date, date: r.date,
start_time: r.starttime, time: match (r.starttime, r.endtime) {
end_time: r.endtime, (Some(start), Some(end)) => AvailabilityTime::Temporarily(start, end),
(_, _) => AvailabilityTime::WholeDay,
},
comment: r.comment.clone(), comment: r.comment.clone(),
}) })
}); });
@ -176,8 +187,10 @@ impl Availability {
user_id: record.userid, user_id: record.userid,
user: None, user: None,
date: record.date, date: record.date,
start_time: record.starttime, time: match (record.starttime, record.endtime) {
end_time: record.endtime, (Some(start), Some(end)) => AvailabilityTime::Temporarily(start, end),
(_, _) => AvailabilityTime::WholeDay,
},
comment: record.comment.clone(), comment: record.comment.clone(),
}) })
}); });
@ -247,8 +260,10 @@ impl Availability {
receive_notifications: r.receivenotifications, receive_notifications: r.receivenotifications,
}), }),
date: r.date, date: r.date,
start_time: r.starttime, time: match (r.starttime, r.endtime) {
end_time: r.endtime, (Some(start), Some(end)) => AvailabilityTime::Temporarily(start, end),
(_, _) => AvailabilityTime::WholeDay,
},
comment: r.comment.clone(), comment: r.comment.clone(),
}) })
.collect(); .collect();
@ -256,18 +271,60 @@ impl Availability {
Ok(availabillities) Ok(availabillities)
} }
pub async fn update( pub async fn read_by_user_and_date(
pool: &PgPool, pool: &PgPool,
id: i32, user_id: i32,
start_time: Option<NaiveTime>, date: &NaiveDate,
end_time: Option<NaiveTime>, ) -> Result<Vec<Availability>> {
comment: Option<&String>, let records = query!(
) -> Result<()> { 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!( query!(
"UPDATE availabillity SET startTime = $1, endTime = $2, comment = $3 WHERE id = $4", "UPDATE availabillity SET startTime = $1, endTime = $2, comment = $3 WHERE id = $4",
start_time, start,
end_time, end,
comment, changeset.comment,
id id
) )
.execute(pool) .execute(pool)

View File

@ -1,6 +1,7 @@
mod area; mod area;
mod assignement; mod assignement;
mod assignment_changeset; mod assignment_changeset;
mod availability_changeset;
mod availabillity; mod availabillity;
mod availabillity_assignment_state; mod availabillity_assignment_state;
mod event; mod event;
@ -13,12 +14,15 @@ mod role;
mod user; mod user;
mod vehicle; mod vehicle;
mod vehicle_assignement; mod vehicle_assignement;
mod availability_changeset;
pub use area::Area; pub use area::Area;
pub use assignement::Assignment; pub use assignement::Assignment;
pub use assignment_changeset::{AssignmentChangeset, AssignmentContext}; 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; pub use availabillity_assignment_state::AvailabillityAssignmentState;
use chrono::NaiveTime; use chrono::NaiveTime;
pub use event::Event; pub use event::Event;

View File

@ -19,24 +19,21 @@
</div> </div>
<div class="field-body"> <div class="field-body">
<div class="field"> <div class="field">
<div class="control"> <div class="control" hx-target="closest body">
<label class="radio"> <label class="radio">
{% if id.is_some() %} {% let time_url -%}
<input type="radio" name="hasTime" hx-get="/availabillity/edit/{{ id.unwrap() }}?wholeday=true" {% if id.is_some() -%}
hx-target="closest body" {{ whole_day|cond_show("checked") }} /> {% let time_url = "/availabillity/edit/{}?wholeday="|format(id.unwrap()) -%}
{% else %} {% else -%}
<input type="radio" name="hasTime" hx-get="/availabillity/new?date={{ date }}&wholeday=true" {% let time_url = "/availabillity/new?date={}&wholeday="|format(date) -%}
hx-target="closest body" {{ whole_day|cond_show("checked") }} /> {% endif -%}
{% endif %} <input type="radio" name="hasTime" hx-get="{{ time_url }}true" {{
whole_day_selected|cond_show("checked") }} />
ganztägig ganztägig
</label>
<label class="radio"> <label class="radio">
{% if id.is_some() %} <input type="radio" name="hasTime" hx-get="{{ time_url }}false" {{
<input type="radio" name="hasTime" hx-get="/availabillity/edit/{{ id.unwrap() }}?wholeday=false" whole_day_selected|invert|ref|cond_show("checked") }} />
hx-target="closest body" {{ whole_day|invert|ref|cond_show("checked") }} />
{% else %}
<input type="radio" name="hasTime" hx-get="/availabillity/new?date={{ date }}&wholeday=false"
hx-target="closest body" {{ whole_day|invert|ref|cond_show("checked") }} />
{% endif %}
zeitweise zeitweise
</label> </label>
</div> </div>
@ -49,9 +46,16 @@
<label class="label">Von - Bis</label> <label class="label">Von - Bis</label>
</div> </div>
<div class="field-body"> <div class="field-body">
{% 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 -%}
<div class="field"> <div class="field">
<input class="input" type="time" id="from" name="from" value='{{ start_time.unwrap_or("00:00") }}' {{ <input class="input" type="time" id="from" name="from" value='{{ times.0 }}' {{
whole_day|cond_show("disabled") }} {{ whole_day|invert|ref|cond_show("required") }}> whole_day_selected|cond_show("disabled") }} {{ whole_day_selected|invert|ref|cond_show("required") }}>
<p class="help">noch mögliche Zeiträume:</p> <p class="help">noch mögliche Zeiträume:</p>
<div class="tags help"> <div class="tags help">
{% for (s, e) in slot_suggestions %} {% for (s, e) in slot_suggestions %}
@ -60,8 +64,8 @@
</div> </div>
</div> </div>
<div class="field"> <div class="field">
<input class="input" type="time" id="till" name="till" value='{{ end_time.unwrap_or("23:59") }}' {{ <input class="input" type="time" id="till" name="till" value='{{ times.1 }}' {{
whole_day|cond_show("disabled") }} {{ whole_day|invert|ref|cond_show("required") }}> whole_day_selected|cond_show("disabled") }} {{ whole_day_selected|invert|ref|cond_show("required") }}>
</div> </div>
</div> </div>
</div> </div>

View File

@ -27,12 +27,12 @@
{{ u.function|show_tree|safe }} {{ u.function|show_tree|safe }}
</td> </td>
<td> <td>
{% if availabillity.start_time.is_some() && availabillity.end_time.is_some() %} {% match availabillity.time %}
{{ availabillity.start_time.as_ref().unwrap().format("%R") }} bis {{ {% when AvailabilityTime::Temporarily(start, end) %}
availabillity.end_time.as_ref().unwrap().format("%R") }} {{ start.format("%R") }} bis {{ end.format("%R") }}
{% else %} {% when _ %}
ganztägig ganztägig
{% endif %} {% endmatch %}
</td> </td>
<td> <td>
{{ availabillity.comment.as_deref().unwrap_or("") }} {{ availabillity.comment.as_deref().unwrap_or("") }}

View File

@ -220,12 +220,12 @@
{{ u.function|show_tree|safe }} {{ u.function|show_tree|safe }}
</td> </td>
<td> <td>
{% if availabillity.start_time.is_some() && availabillity.end_time.is_some() %} {% match availabillity.time %}
{{ availabillity.start_time.as_ref().unwrap().format("%R") }} bis {{ {% when AvailabilityTime::Temporarily(start, end) %}
availabillity.end_time.as_ref().unwrap().format("%R") }} {{ start.format("%R") }} bis {{ end.format("%R") }}
{% else %} {% when _ %}
ganztägig ganztägig
{% endif %} {% endmatch %}
</td> </td>
<td> <td>
{{ availabillity.comment.as_deref().unwrap_or("") }} {{ availabillity.comment.as_deref().unwrap_or("") }}