feat: WIP availability changeset
This commit is contained in:
parent
09821c4b8d
commit
1066b877d5
53
.sqlx/query-5ccea87a5c61aa6ae0fa7a824a61163668ad8e7b2153437f803a1f344d16f62b.json
generated
Normal file
53
.sqlx/query-5ccea87a5c61aa6ae0fa7a824a61163668ad8e7b2153437f803a1f344d16f62b.json
generated
Normal 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"
|
||||
}
|
@ -2,7 +2,7 @@ use rinja::Template;
|
||||
|
||||
use crate::{
|
||||
filters,
|
||||
models::{Availability, AvailabillityAssignmentState, Event, Function},
|
||||
models::{Availability, AvailabilityTime, AvailabillityAssignmentState, Event, Function},
|
||||
};
|
||||
|
||||
pub mod delete;
|
||||
|
@ -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<PgPool>,
|
||||
query: web::Query<AvailabilityNewQuery>,
|
||||
) -> Result<impl Responder, ApplicationError> {
|
||||
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()?))
|
||||
|
@ -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(
|
||||
|
@ -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<IdPath>,
|
||||
query: web::Query<EditAvailabilityQuery>,
|
||||
) -> 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());
|
||||
};
|
||||
|
||||
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()?))
|
||||
|
@ -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<i32>,
|
||||
start_time: Option<&'a str>,
|
||||
end_time: Option<&'a str>,
|
||||
time: Option<AvailabilityTime>,
|
||||
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
|
||||
}
|
||||
|
@ -22,15 +22,16 @@ pub async fn post(
|
||||
pool: web::Data<PgPool>,
|
||||
form: web::Form<AvailabillityForm>,
|
||||
) -> Result<impl Responder, ApplicationError> {
|
||||
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()
|
||||
|
@ -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()
|
||||
|
@ -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,
|
||||
|
@ -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()
|
||||
|
@ -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<i32>,
|
||||
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);
|
||||
}
|
||||
|
||||
|
@ -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()?;
|
||||
|
@ -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",
|
||||
));
|
||||
|
@ -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
|
||||
}
|
||||
|
@ -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<User>,
|
||||
pub date: NaiveDate,
|
||||
pub start_time: Option<NaiveTime>,
|
||||
pub end_time: Option<NaiveTime>,
|
||||
pub time: AvailabilityTime,
|
||||
pub comment: Option<String>,
|
||||
}
|
||||
|
||||
#[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<NaiveTime>,
|
||||
end_time: Option<NaiveTime>,
|
||||
comment: Option<String>,
|
||||
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<Option<Availability>> {
|
||||
pub async fn read_by_id_including_user(pool: &PgPool, id: i32) -> Result<Option<Availability>> {
|
||||
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<NaiveTime>,
|
||||
end_time: Option<NaiveTime>,
|
||||
comment: Option<&String>,
|
||||
) -> Result<()> {
|
||||
user_id: i32,
|
||||
date: &NaiveDate,
|
||||
) -> Result<Vec<Availability>> {
|
||||
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)
|
||||
|
@ -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;
|
||||
|
@ -19,26 +19,23 @@
|
||||
</div>
|
||||
<div class="field-body">
|
||||
<div class="field">
|
||||
<div class="control">
|
||||
<div class="control" hx-target="closest body">
|
||||
<label class="radio">
|
||||
{% if id.is_some() %}
|
||||
<input type="radio" name="hasTime" hx-get="/availabillity/edit/{{ id.unwrap() }}?wholeday=true"
|
||||
hx-target="closest body" {{ whole_day|cond_show("checked") }} />
|
||||
{% else %}
|
||||
<input type="radio" name="hasTime" hx-get="/availabillity/new?date={{ date }}&wholeday=true"
|
||||
hx-target="closest body" {{ whole_day|cond_show("checked") }} />
|
||||
{% endif %}
|
||||
{% let time_url -%}
|
||||
{% if id.is_some() -%}
|
||||
{% let time_url = "/availabillity/edit/{}?wholeday="|format(id.unwrap()) -%}
|
||||
{% else -%}
|
||||
{% let time_url = "/availabillity/new?date={}&wholeday="|format(date) -%}
|
||||
{% endif -%}
|
||||
<input type="radio" name="hasTime" hx-get="{{ time_url }}true" {{
|
||||
whole_day_selected|cond_show("checked") }} />
|
||||
ganztägig
|
||||
<label class="radio">
|
||||
{% if id.is_some() %}
|
||||
<input type="radio" name="hasTime" hx-get="/availabillity/edit/{{ id.unwrap() }}?wholeday=false"
|
||||
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
|
||||
</label>
|
||||
</label>
|
||||
<label class="radio">
|
||||
<input type="radio" name="hasTime" hx-get="{{ time_url }}false" {{
|
||||
whole_day_selected|invert|ref|cond_show("checked") }} />
|
||||
zeitweise
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@ -49,9 +46,16 @@
|
||||
<label class="label">Von - Bis</label>
|
||||
</div>
|
||||
<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">
|
||||
<input class="input" type="time" id="from" name="from" value='{{ start_time.unwrap_or("00:00") }}' {{
|
||||
whole_day|cond_show("disabled") }} {{ whole_day|invert|ref|cond_show("required") }}>
|
||||
<input class="input" type="time" id="from" name="from" value='{{ times.0 }}' {{
|
||||
whole_day_selected|cond_show("disabled") }} {{ whole_day_selected|invert|ref|cond_show("required") }}>
|
||||
|
||||
<p class="help">noch mögliche Zeiträume:</p>
|
||||
<div class="tags help">
|
||||
{% for (s, e) in slot_suggestions %}
|
||||
@ -60,8 +64,8 @@
|
||||
</div>
|
||||
</div>
|
||||
<div class="field">
|
||||
<input class="input" type="time" id="till" name="till" value='{{ end_time.unwrap_or("23:59") }}' {{
|
||||
whole_day|cond_show("disabled") }} {{ whole_day|invert|ref|cond_show("required") }}>
|
||||
<input class="input" type="time" id="till" name="till" value='{{ times.1 }}' {{
|
||||
whole_day_selected|cond_show("disabled") }} {{ whole_day_selected|invert|ref|cond_show("required") }}>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
@ -27,12 +27,12 @@
|
||||
{{ u.function|show_tree|safe }}
|
||||
</td>
|
||||
<td>
|
||||
{% 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 %}
|
||||
</td>
|
||||
<td>
|
||||
{{ availabillity.comment.as_deref().unwrap_or("") }}
|
||||
|
@ -220,12 +220,12 @@
|
||||
{{ u.function|show_tree|safe }}
|
||||
</td>
|
||||
<td>
|
||||
{% 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 %}
|
||||
</td>
|
||||
<td>
|
||||
{{ availabillity.comment.as_deref().unwrap_or("") }}
|
||||
|
Loading…
x
Reference in New Issue
Block a user