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::{
filters,
models::{Availability, AvailabillityAssignmentState, Event, Function},
models::{Availability, AvailabilityTime, AvailabillityAssignmentState, Event, Function},
};
pub mod delete;

View File

@ -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()?))

View File

@ -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(

View File

@ -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()?))

View File

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

View File

@ -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()

View File

@ -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()

View File

@ -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,

View File

@ -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()

View File

@ -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);
}

View File

@ -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()?;

View File

@ -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",
));

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 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)

View File

@ -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;

View File

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

View File

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

View File

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