feat: only allow one availability for a time

This commit is contained in:
Max Hohlfeld 2025-01-26 19:21:34 +01:00
parent 2cae816741
commit 864141121a
7 changed files with 160 additions and 27 deletions

View File

@ -2,9 +2,10 @@ use actix_web::{web, HttpResponse, Responder};
use chrono::NaiveDate;
use rinja::Template;
use serde::Deserialize;
use sqlx::PgPool;
use crate::endpoints::availability::NewOrEditAvailabilityTemplate;
use crate::models::User;
use crate::endpoints::availability::{calc_free_slots_cor, NewOrEditAvailabilityTemplate};
use crate::models::{Availabillity, User};
use crate::utils::ApplicationError;
#[derive(Deserialize)]
@ -17,8 +18,32 @@ struct AvailabilityNewQuery {
#[actix_web::get("/availabillity/new")]
pub async fn get(
user: web::ReqData<User>,
pool: web::Data<PgPool>,
query: web::Query<AvailabilityNewQuery>,
) -> 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 {
user: user.into_inner(),
date: query.date,
@ -27,6 +52,7 @@ pub async fn get(
start_time: None,
end_time: None,
comment: None,
slot_suggestions: calc_free_slots_cor(&availabilities_from_user),
};
Ok(HttpResponse::Ok().body(template.render()?))

View File

@ -1,10 +1,8 @@
use crate::{
filters,
models::{Assignment, Function, Vehicle},
utils::{event_planning_template::generate_vehicles_assigned_and_available, ApplicationError},
endpoints::availability::calc_free_slots_cor, filters, models::{Assignment, Function, Vehicle}, utils::{event_planning_template::generate_vehicles_assigned_and_available, ApplicationError}
};
use actix_web::{web, HttpResponse, Responder};
use chrono::{NaiveDate, Utc};
use chrono::{NaiveDate, NaiveTime, Utc};
use rinja::Template;
use serde::Deserialize;
use sqlx::PgPool;
@ -21,10 +19,17 @@ pub struct CalendarQuery {
#[template(path = "index.html")]
struct CalendarTemplate {
user: User,
user_can_create_availabillity: bool,
date: NaiveDate,
selected_area: Option<i32>,
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>,
}
@ -59,6 +64,18 @@ async fn get(
)
.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();
for e in Event::read_all_by_date_and_area_including_location(
pool.get_ref(),
@ -120,12 +137,13 @@ async fn get(
.clone(),
)
}),
assigned_vehicle
assigned_vehicle,
));
}
let template = CalendarTemplate {
user: user.into_inner(),
user_can_create_availabillity,
date,
selected_area,
areas,

View File

@ -4,7 +4,10 @@ use serde::Deserialize;
use sqlx::PgPool;
use crate::{
endpoints::{availability::NewOrEditAvailabilityTemplate, IdPath},
endpoints::{
availability::{calc_free_slots_cor, NewOrEditAvailabilityTemplate},
IdPath,
},
models::{Availabillity, User},
utils::ApplicationError,
};
@ -40,6 +43,29 @@ pub async fn get(
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 {
user: user.into_inner(),
date: availabillity.date,
@ -48,6 +74,7 @@ pub async fn get(
start_time: start_time.as_deref(),
end_time: end_time.as_deref(),
comment: availabillity.comment.as_deref(),
slot_suggestions: suggestions,
};
Ok(HttpResponse::Ok().body(template.render()?))

View File

@ -1,8 +1,8 @@
use chrono::{NaiveDate, NaiveTime};
use rinja::Template;
use chrono::NaiveDate;
use crate::filters;
use crate::models::{Role, User};
use crate::models::{Availabillity, Role, User};
pub mod delete;
pub mod get_new;
@ -21,4 +21,72 @@ struct NewOrEditAvailabilityTemplate<'a> {
start_time: Option<&'a str>,
end_time: 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
}

View File

@ -1,5 +1,3 @@
use std::fmt::Display;
use chrono::{NaiveDate, NaiveTime};
use sqlx::{query, PgPool};
@ -30,18 +28,6 @@ pub enum AvailabillityAssignmentState {
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 {
pub async fn create(
pool: &PgPool,

View File

@ -52,6 +52,12 @@
<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") }}>
<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 class="field">
<input class="input" type="time" id="till" name="till" value='{{ end_time.unwrap_or("23:59") }}' {{

View File

@ -183,12 +183,14 @@
</div>
{% if selected_area.is_none() || selected_area.unwrap() == user.area_id %}
<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">
<use href="/static/feather-sprite.svg#plus-circle" />
</svg>
<span>Neue Verfügbarkeit für diesen Tag</span>
</a>
</button>
</div>
{% endif %}
</div>