feat: only allow one availability for a time
This commit is contained in:
parent
2cae816741
commit
864141121a
@ -2,9 +2,10 @@ use actix_web::{web, HttpResponse, Responder};
|
|||||||
use chrono::NaiveDate;
|
use chrono::NaiveDate;
|
||||||
use rinja::Template;
|
use rinja::Template;
|
||||||
use serde::Deserialize;
|
use serde::Deserialize;
|
||||||
|
use sqlx::PgPool;
|
||||||
|
|
||||||
use crate::endpoints::availability::NewOrEditAvailabilityTemplate;
|
use crate::endpoints::availability::{calc_free_slots_cor, NewOrEditAvailabilityTemplate};
|
||||||
use crate::models::User;
|
use crate::models::{Availabillity, User};
|
||||||
use crate::utils::ApplicationError;
|
use crate::utils::ApplicationError;
|
||||||
|
|
||||||
#[derive(Deserialize)]
|
#[derive(Deserialize)]
|
||||||
@ -17,8 +18,32 @@ struct AvailabilityNewQuery {
|
|||||||
#[actix_web::get("/availabillity/new")]
|
#[actix_web::get("/availabillity/new")]
|
||||||
pub async fn get(
|
pub async fn get(
|
||||||
user: web::ReqData<User>,
|
user: web::ReqData<User>,
|
||||||
|
pool: web::Data<PgPool>,
|
||||||
query: web::Query<AvailabilityNewQuery>,
|
query: web::Query<AvailabilityNewQuery>,
|
||||||
) -> Result<impl Responder, ApplicationError> {
|
) -> Result<impl Responder, ApplicationError> {
|
||||||
|
let availabillities = Availabillity::read_by_date_and_area_including_user(
|
||||||
|
pool.get_ref(),
|
||||||
|
query.date,
|
||||||
|
user.area_id,
|
||||||
|
)
|
||||||
|
.await?;
|
||||||
|
|
||||||
|
let availabilities_from_user: Vec<&Availabillity> = 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);
|
||||||
|
|
||||||
|
if !user_can_create_availabillity {
|
||||||
|
return Ok(HttpResponse::BadRequest().finish());
|
||||||
|
}
|
||||||
|
|
||||||
let template = NewOrEditAvailabilityTemplate {
|
let template = NewOrEditAvailabilityTemplate {
|
||||||
user: user.into_inner(),
|
user: user.into_inner(),
|
||||||
date: query.date,
|
date: query.date,
|
||||||
@ -27,6 +52,7 @@ pub async fn get(
|
|||||||
start_time: None,
|
start_time: None,
|
||||||
end_time: None,
|
end_time: None,
|
||||||
comment: None,
|
comment: None,
|
||||||
|
slot_suggestions: calc_free_slots_cor(&availabilities_from_user),
|
||||||
};
|
};
|
||||||
|
|
||||||
Ok(HttpResponse::Ok().body(template.render()?))
|
Ok(HttpResponse::Ok().body(template.render()?))
|
||||||
|
@ -1,10 +1,8 @@
|
|||||||
use crate::{
|
use crate::{
|
||||||
filters,
|
endpoints::availability::calc_free_slots_cor, filters, models::{Assignment, Function, Vehicle}, utils::{event_planning_template::generate_vehicles_assigned_and_available, ApplicationError}
|
||||||
models::{Assignment, 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, Utc};
|
use chrono::{NaiveDate, NaiveTime, Utc};
|
||||||
use rinja::Template;
|
use rinja::Template;
|
||||||
use serde::Deserialize;
|
use serde::Deserialize;
|
||||||
use sqlx::PgPool;
|
use sqlx::PgPool;
|
||||||
@ -21,10 +19,17 @@ pub struct CalendarQuery {
|
|||||||
#[template(path = "index.html")]
|
#[template(path = "index.html")]
|
||||||
struct CalendarTemplate {
|
struct CalendarTemplate {
|
||||||
user: User,
|
user: User,
|
||||||
|
user_can_create_availabillity: bool,
|
||||||
date: NaiveDate,
|
date: NaiveDate,
|
||||||
selected_area: Option<i32>,
|
selected_area: Option<i32>,
|
||||||
areas: Vec<Area>,
|
areas: Vec<Area>,
|
||||||
events_and_assignments: Vec<(Event, Vec<String>, Option<String>, Option<String>, Vec<Vehicle>)>,
|
events_and_assignments: Vec<(
|
||||||
|
Event,
|
||||||
|
Vec<String>,
|
||||||
|
Option<String>,
|
||||||
|
Option<String>,
|
||||||
|
Vec<Vehicle>,
|
||||||
|
)>,
|
||||||
availabillities: Vec<Availabillity>,
|
availabillities: Vec<Availabillity>,
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -59,6 +64,18 @@ async fn get(
|
|||||||
)
|
)
|
||||||
.await?;
|
.await?;
|
||||||
|
|
||||||
|
let availabilities_from_user: Vec<&Availabillity> = 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 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(
|
||||||
pool.get_ref(),
|
pool.get_ref(),
|
||||||
@ -120,12 +137,13 @@ async fn get(
|
|||||||
.clone(),
|
.clone(),
|
||||||
)
|
)
|
||||||
}),
|
}),
|
||||||
assigned_vehicle
|
assigned_vehicle,
|
||||||
));
|
));
|
||||||
}
|
}
|
||||||
|
|
||||||
let template = CalendarTemplate {
|
let template = CalendarTemplate {
|
||||||
user: user.into_inner(),
|
user: user.into_inner(),
|
||||||
|
user_can_create_availabillity,
|
||||||
date,
|
date,
|
||||||
selected_area,
|
selected_area,
|
||||||
areas,
|
areas,
|
||||||
|
@ -4,7 +4,10 @@ use serde::Deserialize;
|
|||||||
use sqlx::PgPool;
|
use sqlx::PgPool;
|
||||||
|
|
||||||
use crate::{
|
use crate::{
|
||||||
endpoints::{availability::NewOrEditAvailabilityTemplate, IdPath},
|
endpoints::{
|
||||||
|
availability::{calc_free_slots_cor, NewOrEditAvailabilityTemplate},
|
||||||
|
IdPath,
|
||||||
|
},
|
||||||
models::{Availabillity, User},
|
models::{Availabillity, User},
|
||||||
utils::ApplicationError,
|
utils::ApplicationError,
|
||||||
};
|
};
|
||||||
@ -40,6 +43,29 @@ pub async fn get(
|
|||||||
|
|
||||||
let has_time = availabillity.start_time.is_some() && availabillity.end_time.is_some();
|
let has_time = availabillity.start_time.is_some() && availabillity.end_time.is_some();
|
||||||
|
|
||||||
|
let suggestions = if has_time {
|
||||||
|
let availabillities = Availabillity::read_by_date_and_area_including_user(
|
||||||
|
pool.get_ref(),
|
||||||
|
availabillity.date,
|
||||||
|
user.area_id,
|
||||||
|
)
|
||||||
|
.await?;
|
||||||
|
|
||||||
|
let availabilities_from_user: Vec<&Availabillity> = availabillities
|
||||||
|
.iter()
|
||||||
|
.filter(|a| a.user_id == user.id)
|
||||||
|
.collect();
|
||||||
|
|
||||||
|
calc_free_slots_cor(&availabilities_from_user)
|
||||||
|
.into_iter()
|
||||||
|
.filter(|(a, b)| {
|
||||||
|
*b == availabillity.start_time.unwrap() || *a == availabillity.end_time.unwrap()
|
||||||
|
})
|
||||||
|
.collect()
|
||||||
|
} else {
|
||||||
|
Vec::new()
|
||||||
|
};
|
||||||
|
|
||||||
let template = NewOrEditAvailabilityTemplate {
|
let template = NewOrEditAvailabilityTemplate {
|
||||||
user: user.into_inner(),
|
user: user.into_inner(),
|
||||||
date: availabillity.date,
|
date: availabillity.date,
|
||||||
@ -48,6 +74,7 @@ pub async fn get(
|
|||||||
start_time: start_time.as_deref(),
|
start_time: start_time.as_deref(),
|
||||||
end_time: end_time.as_deref(),
|
end_time: end_time.as_deref(),
|
||||||
comment: availabillity.comment.as_deref(),
|
comment: availabillity.comment.as_deref(),
|
||||||
|
slot_suggestions: suggestions,
|
||||||
};
|
};
|
||||||
|
|
||||||
Ok(HttpResponse::Ok().body(template.render()?))
|
Ok(HttpResponse::Ok().body(template.render()?))
|
||||||
|
@ -1,8 +1,8 @@
|
|||||||
|
use chrono::{NaiveDate, NaiveTime};
|
||||||
use rinja::Template;
|
use rinja::Template;
|
||||||
use chrono::NaiveDate;
|
|
||||||
|
|
||||||
use crate::filters;
|
use crate::filters;
|
||||||
use crate::models::{Role, User};
|
use crate::models::{Availabillity, Role, User};
|
||||||
|
|
||||||
pub mod delete;
|
pub mod delete;
|
||||||
pub mod get_new;
|
pub mod get_new;
|
||||||
@ -21,4 +21,72 @@ struct NewOrEditAvailabilityTemplate<'a> {
|
|||||||
start_time: Option<&'a str>,
|
start_time: Option<&'a str>,
|
||||||
end_time: Option<&'a str>,
|
end_time: Option<&'a str>,
|
||||||
comment: Option<&'a str>,
|
comment: Option<&'a str>,
|
||||||
|
slot_suggestions: Vec<(NaiveTime, NaiveTime)>,
|
||||||
|
}
|
||||||
|
|
||||||
|
fn calc_free_slots_cor(availabilities: &Vec<&Availabillity>) -> 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
|
||||||
}
|
}
|
||||||
|
@ -1,5 +1,3 @@
|
|||||||
use std::fmt::Display;
|
|
||||||
|
|
||||||
use chrono::{NaiveDate, NaiveTime};
|
use chrono::{NaiveDate, NaiveTime};
|
||||||
use sqlx::{query, PgPool};
|
use sqlx::{query, PgPool};
|
||||||
|
|
||||||
@ -30,18 +28,6 @@ pub enum AvailabillityAssignmentState {
|
|||||||
AssignedWachhabender(i32),
|
AssignedWachhabender(i32),
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Display for AvailabillityAssignmentState {
|
|
||||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
|
||||||
match self {
|
|
||||||
AvailabillityAssignmentState::Unassigned => write!(f, "nicht zugewiesen"),
|
|
||||||
AvailabillityAssignmentState::Conflicting => write!(f, "bereits anders zugewiesen"),
|
|
||||||
AvailabillityAssignmentState::AssignedPosten(_) => write!(f, "zugewiesen als Posten"),
|
|
||||||
AvailabillityAssignmentState::AssignedFührungsassistent(_) => write!(f, "zugewiesen als Führungsassistent"),
|
|
||||||
AvailabillityAssignmentState::AssignedWachhabender(_) => write!(f, "zugewiesen als Wachhabender"),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Availabillity {
|
impl Availabillity {
|
||||||
pub async fn create(
|
pub async fn create(
|
||||||
pool: &PgPool,
|
pool: &PgPool,
|
||||||
|
@ -52,6 +52,12 @@
|
|||||||
<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='{{ start_time.unwrap_or("00:00") }}' {{
|
||||||
whole_day|cond_show("disabled") }} {{ whole_day|invert|ref|cond_show("required") }}>
|
whole_day|cond_show("disabled") }} {{ whole_day|invert|ref|cond_show("required") }}>
|
||||||
|
<p class="help">noch mögliche Zeiträume:</p>
|
||||||
|
<div class="tags help">
|
||||||
|
{% for (s, e) in slot_suggestions %}
|
||||||
|
<span class="tag">{{ s }} - {{ e }}</span>
|
||||||
|
{% endfor %}
|
||||||
|
</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='{{ end_time.unwrap_or("23:59") }}' {{
|
||||||
|
@ -183,12 +183,14 @@
|
|||||||
</div>
|
</div>
|
||||||
{% if selected_area.is_none() || selected_area.unwrap() == user.area_id %}
|
{% if selected_area.is_none() || selected_area.unwrap() == user.area_id %}
|
||||||
<div class="level-right">
|
<div class="level-right">
|
||||||
<a class="button is-link is-light" hx-boost="true" href="/availabillity/new?date={{ date }}">
|
{% let btn_disabled = !user_can_create_availabillity %}
|
||||||
|
<button class="button is-link is-light" hx-get="/availabillity/new?date={{ date }}" {{
|
||||||
|
btn_disabled|cond_show("disabled") }} hx-target="closest body">
|
||||||
<svg class="icon">
|
<svg class="icon">
|
||||||
<use href="/static/feather-sprite.svg#plus-circle" />
|
<use href="/static/feather-sprite.svg#plus-circle" />
|
||||||
</svg>
|
</svg>
|
||||||
<span>Neue Verfügbarkeit für diesen Tag</span>
|
<span>Neue Verfügbarkeit für diesen Tag</span>
|
||||||
</a>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
</div>
|
</div>
|
||||||
|
Loading…
x
Reference in New Issue
Block a user