diff --git a/db/.sqlx/query-5f9e0e2f5037716e03409d751a32a64a2ae7393109c6cf8d46d992dc2c1f4713.json b/db/.sqlx/query-5f9e0e2f5037716e03409d751a32a64a2ae7393109c6cf8d46d992dc2c1f4713.json new file mode 100644 index 00000000..89a026e5 --- /dev/null +++ b/db/.sqlx/query-5f9e0e2f5037716e03409d751a32a64a2ae7393109c6cf8d46d992dc2c1f4713.json @@ -0,0 +1,15 @@ +{ + "db_name": "PostgreSQL", + "query": "UPDATE availability SET crossAreal = $1 WHERE id = $2;", + "describe": { + "columns": [], + "parameters": { + "Left": [ + "Bool", + "Int4" + ] + }, + "nullable": [] + }, + "hash": "5f9e0e2f5037716e03409d751a32a64a2ae7393109c6cf8d46d992dc2c1f4713" +} diff --git a/db/src/models/availability.rs b/db/src/models/availability.rs index 488bbee5..5e8c5e31 100644 --- a/db/src/models/availability.rs +++ b/db/src/models/availability.rs @@ -331,6 +331,17 @@ impl Availability { Ok(()) } + pub async fn update_cross_areal(pool: &PgPool, id: i32, cross_areal: bool) -> Result<()> { + query!( + "UPDATE availability SET crossAreal = $1 WHERE id = $2;", + cross_areal, + id + ) + .execute(pool) + .await?; + Ok(()) + } + pub async fn delete(pool: &PgPool, id: i32) -> Result<()> { query!("DELETE FROM availability WHERE id = $1", id) .execute(pool) diff --git a/web/src/endpoints/availability/mod.rs b/web/src/endpoints/availability/mod.rs index 75d4bea7..b5dec38b 100644 --- a/web/src/endpoints/availability/mod.rs +++ b/web/src/endpoints/availability/mod.rs @@ -16,6 +16,7 @@ pub mod get_overview; pub mod get_update; pub mod post_new; pub mod post_update; +pub mod put_cross_areal; #[derive(Template)] #[template(path = "availability/new_or_edit.html")] @@ -30,7 +31,7 @@ struct NewOrEditAvailabilityTemplate<'a> { slot_suggestions: Vec<(NaiveDateTime, NaiveDateTime)>, datetomorrow: NaiveDate, other_user: Option, - other_users: Vec + other_users: Vec, } #[derive(Deserialize)] @@ -40,5 +41,5 @@ pub struct AvailabilityForm { pub starttime: NaiveTime, pub endtime: NaiveTime, pub comment: Option, - pub user: Option + pub user: Option, } diff --git a/web/src/endpoints/availability/put_cross_areal.rs b/web/src/endpoints/availability/put_cross_areal.rs new file mode 100644 index 00000000..be683a09 --- /dev/null +++ b/web/src/endpoints/availability/put_cross_areal.rs @@ -0,0 +1,122 @@ +use actix_web::{web, HttpResponse, Responder}; +use askama::Template; +use serde_json::json; +use sqlx::PgPool; + +use crate::{ + endpoints::IdPath, + utils::{ApplicationError, TemplateResponse}, +}; +use brass_db::models::{Assignment, Availability, Role, User}; + +#[derive(Template)] +#[template(path = "calendar_cross_areal_button.html")] +struct CalendarPartialCrossArealButtonTemplate { + availability: Availability, +} + +#[actix_web::put("/availability/{id}/makeNonCrossAreal")] +pub async fn put_non_cross_areal( + user: web::ReqData, + pool: web::Data, + path: web::Path, +) -> Result { + handle_cross_areal(user, pool, path, false).await +} + +#[actix_web::put("/availability/{id}/makeCrossAreal")] +pub async fn put_cross_areal( + user: web::ReqData, + pool: web::Data, + path: web::Path, +) -> Result { + handle_cross_areal(user, pool, path, true).await +} + +async fn handle_cross_areal( + user: web::ReqData, + pool: web::Data, + path: web::Path, + cross_areal: bool, +) -> Result { + if user.role != Role::Admin && user.role != Role::AreaManager { + return Err(ApplicationError::Unauthorized); + } + + let Some(mut availability) = Availability::read_including_user(pool.get_ref(), path.id).await? + else { + return Ok(HttpResponse::NotFound().finish()); + }; + + if user.role == Role::AreaManager && availability.user.as_ref().unwrap().area_id != user.area_id + { + return Err(ApplicationError::Unauthorized); + } + + let assignments_for_availability = + Assignment::read_all_by_availability(pool.get_ref(), availability.id).await?; + + if !cross_areal && assignments_for_availability.len() != 0 { + let trigger = json!({ + "showToast": { + "type": "danger", + "message": "Verfügbarkeit bereits verplant!" + } + }) + .to_string(); + + return Ok(HttpResponse::UnprocessableEntity() + .insert_header(("HX-TRIGGER", trigger)) + .finish()); + } + + if availability.cross_areal != cross_areal { + Availability::update_cross_areal(pool.get_ref(), availability.id, cross_areal).await?; + availability.cross_areal = cross_areal; + } + + let template = CalendarPartialCrossArealButtonTemplate { availability }; + Ok(template.to_response()?) +} + +#[cfg(test)] +mod tests { + // use crate::utils::test_helper::{ + // assert_snapshot, create_test_login_user, read_body, test_put, DbTestContext, RequestConfig, + // StatusCode, + // }; + // use brass_macros::db_test; + // + // #[db_test] + // async fn user_can_toggle_subscription_for_himself(context: &DbTestContext) { + // let app = context.app().await; + // + // let unsubscribe_config = RequestConfig::new("/users/1/unsubscribeNotifications"); + // create_test_login_user(&context.db_pool, &unsubscribe_config).await; + // let unsubscribe_response = test_put::<_, _, String>(&app, &unsubscribe_config, None).await; + // + // assert_eq!(StatusCode::OK, unsubscribe_response.status()); + // + // let unsubscribe_body = read_body(unsubscribe_response).await; + // assert_snapshot!(unsubscribe_body); + // + // let subscribe_config = RequestConfig::new("/users/1/subscribeNotifications"); + // let subscribe_response = test_put::<_, _, String>(&app, &subscribe_config, None).await; + // + // assert_eq!(StatusCode::OK, subscribe_response.status()); + // + // let subscribe_body = read_body(subscribe_response).await; + // assert_snapshot!(subscribe_body); + // } + // + // #[db_test] + // async fn user_cant_toggle_subscription_for_someone_else(context: &DbTestContext) { + // let app = context.app().await; + // + // let unsubscribe_config = RequestConfig::new("/users/3/unsubscribeNotifications"); + // create_test_login_user(&context.db_pool, &unsubscribe_config).await; + // let unsubscribe_response = test_put::<_, _, String>(&app, &unsubscribe_config, None).await; + // + // assert_eq!(StatusCode::UNAUTHORIZED, unsubscribe_response.status()); + // } +} diff --git a/web/src/endpoints/mod.rs b/web/src/endpoints/mod.rs index 28878aa2..6ccb71dc 100644 --- a/web/src/endpoints/mod.rs +++ b/web/src/endpoints/mod.rs @@ -62,6 +62,8 @@ pub fn init(cfg: &mut ServiceConfig) { cfg.service(availability::post_new::post); cfg.service(availability::post_update::post); cfg.service(availability::get_overview::get); + cfg.service(availability::put_cross_areal::put_cross_areal); + cfg.service(availability::put_cross_areal::put_non_cross_areal); cfg.service(events::put_cancelation::put_cancel); cfg.service(events::put_cancelation::put_uncancel); diff --git a/web/templates/calendar.html b/web/templates/calendar.html index 4bcc9716..7a8972e4 100644 --- a/web/templates/calendar.html +++ b/web/templates/calendar.html @@ -312,7 +312,7 @@ {{ availability.end|fmt_datetime(DayMonthYearHourMinute) }} Uhr - {{ availability.comment.as_deref().unwrap_or("") }} + {{ availability.comment.as_deref().unwrap_or_default() }} {% if availability.user_id == user.id || user.role == Role::Admin || user.role == Role::AreaManager %} @@ -324,11 +324,16 @@ + {% if user.role == Role::Admin || (user.role == Role::AreaManager && user.area_id == + availability.user.as_ref().unwrap().area_id) %} + {% include "calendar_cross_areal_button.html" %} + {% endif %} {% endif %} diff --git a/web/templates/calendar_cross_areal_button.html b/web/templates/calendar_cross_areal_button.html new file mode 100644 index 00000000..7abb8867 --- /dev/null +++ b/web/templates/calendar_cross_areal_button.html @@ -0,0 +1,8 @@ +