feat: toggle cross areal
This commit is contained in:
parent
5063933f33
commit
290e610058
15
db/.sqlx/query-5f9e0e2f5037716e03409d751a32a64a2ae7393109c6cf8d46d992dc2c1f4713.json
generated
Normal file
15
db/.sqlx/query-5f9e0e2f5037716e03409d751a32a64a2ae7393109c6cf8d46d992dc2c1f4713.json
generated
Normal file
@ -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"
|
||||||
|
}
|
@ -331,6 +331,17 @@ impl Availability {
|
|||||||
Ok(())
|
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<()> {
|
pub async fn delete(pool: &PgPool, id: i32) -> Result<()> {
|
||||||
query!("DELETE FROM availability WHERE id = $1", id)
|
query!("DELETE FROM availability WHERE id = $1", id)
|
||||||
.execute(pool)
|
.execute(pool)
|
||||||
|
@ -16,6 +16,7 @@ pub mod get_overview;
|
|||||||
pub mod get_update;
|
pub mod get_update;
|
||||||
pub mod post_new;
|
pub mod post_new;
|
||||||
pub mod post_update;
|
pub mod post_update;
|
||||||
|
pub mod put_cross_areal;
|
||||||
|
|
||||||
#[derive(Template)]
|
#[derive(Template)]
|
||||||
#[template(path = "availability/new_or_edit.html")]
|
#[template(path = "availability/new_or_edit.html")]
|
||||||
@ -30,7 +31,7 @@ struct NewOrEditAvailabilityTemplate<'a> {
|
|||||||
slot_suggestions: Vec<(NaiveDateTime, NaiveDateTime)>,
|
slot_suggestions: Vec<(NaiveDateTime, NaiveDateTime)>,
|
||||||
datetomorrow: NaiveDate,
|
datetomorrow: NaiveDate,
|
||||||
other_user: Option<i32>,
|
other_user: Option<i32>,
|
||||||
other_users: Vec<User>
|
other_users: Vec<User>,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Deserialize)]
|
#[derive(Deserialize)]
|
||||||
@ -40,5 +41,5 @@ pub struct AvailabilityForm {
|
|||||||
pub starttime: NaiveTime,
|
pub starttime: NaiveTime,
|
||||||
pub endtime: NaiveTime,
|
pub endtime: NaiveTime,
|
||||||
pub comment: Option<String>,
|
pub comment: Option<String>,
|
||||||
pub user: Option<i32>
|
pub user: Option<i32>,
|
||||||
}
|
}
|
||||||
|
122
web/src/endpoints/availability/put_cross_areal.rs
Normal file
122
web/src/endpoints/availability/put_cross_areal.rs
Normal file
@ -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<User>,
|
||||||
|
pool: web::Data<PgPool>,
|
||||||
|
path: web::Path<IdPath>,
|
||||||
|
) -> Result<impl Responder, ApplicationError> {
|
||||||
|
handle_cross_areal(user, pool, path, false).await
|
||||||
|
}
|
||||||
|
|
||||||
|
#[actix_web::put("/availability/{id}/makeCrossAreal")]
|
||||||
|
pub async fn put_cross_areal(
|
||||||
|
user: web::ReqData<User>,
|
||||||
|
pool: web::Data<PgPool>,
|
||||||
|
path: web::Path<IdPath>,
|
||||||
|
) -> Result<impl Responder, ApplicationError> {
|
||||||
|
handle_cross_areal(user, pool, path, true).await
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn handle_cross_areal(
|
||||||
|
user: web::ReqData<User>,
|
||||||
|
pool: web::Data<PgPool>,
|
||||||
|
path: web::Path<IdPath>,
|
||||||
|
cross_areal: bool,
|
||||||
|
) -> Result<impl Responder, ApplicationError> {
|
||||||
|
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());
|
||||||
|
// }
|
||||||
|
}
|
@ -62,6 +62,8 @@ pub fn init(cfg: &mut ServiceConfig) {
|
|||||||
cfg.service(availability::post_new::post);
|
cfg.service(availability::post_new::post);
|
||||||
cfg.service(availability::post_update::post);
|
cfg.service(availability::post_update::post);
|
||||||
cfg.service(availability::get_overview::get);
|
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_cancel);
|
||||||
cfg.service(events::put_cancelation::put_uncancel);
|
cfg.service(events::put_cancelation::put_uncancel);
|
||||||
|
@ -312,7 +312,7 @@
|
|||||||
{{ availability.end|fmt_datetime(DayMonthYearHourMinute) }} Uhr
|
{{ availability.end|fmt_datetime(DayMonthYearHourMinute) }} Uhr
|
||||||
</td>
|
</td>
|
||||||
<td>
|
<td>
|
||||||
{{ availability.comment.as_deref().unwrap_or("") }}
|
{{ availability.comment.as_deref().unwrap_or_default() }}
|
||||||
</td>
|
</td>
|
||||||
<td>
|
<td>
|
||||||
{% if availability.user_id == user.id || user.role == Role::Admin || user.role == Role::AreaManager %}
|
{% if availability.user_id == user.id || user.role == Role::Admin || user.role == Role::AreaManager %}
|
||||||
@ -324,11 +324,16 @@
|
|||||||
</svg>
|
</svg>
|
||||||
</a>
|
</a>
|
||||||
<button class="button is-danger is-light" hx-delete="/availability/delete/{{ availability.id }}"
|
<button class="button is-danger is-light" hx-delete="/availability/delete/{{ availability.id }}"
|
||||||
hx-target="closest tr" hx-swap="delete" hx-trigger="confirmed" title="Verfügbarkeit löschen">
|
hx-target="closest tr" hx-swap="delete" hx-trigger="confirmed"
|
||||||
|
title="Verfügbarkeit löschen">
|
||||||
<svg class="icon">
|
<svg class="icon">
|
||||||
<use href="/static/feather-sprite.svg#x-circle" />
|
<use href="/static/feather-sprite.svg#x-circle" />
|
||||||
</svg>
|
</svg>
|
||||||
</button>
|
</button>
|
||||||
|
{% 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 %}
|
||||||
</div>
|
</div>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
</td>
|
</td>
|
||||||
|
8
web/templates/calendar_cross_areal_button.html
Normal file
8
web/templates/calendar_cross_areal_button.html
Normal file
@ -0,0 +1,8 @@
|
|||||||
|
<button class="button is-link is-light" hx-swap="outerHTML"
|
||||||
|
hx-put="/availability/{{ availability.id }}/make{% if availability.cross_areal %}Non{% endif %}CrossAreal"
|
||||||
|
title="{% if availability.cross_areal %}nur im Hauptbereich{% else %}Bereichsübergreifend{% endif %} verfügbar machen">
|
||||||
|
<svg class="icon">
|
||||||
|
<use
|
||||||
|
href="/static/feather-sprite.svg#{% if availability.cross_areal %}home{% else %}globe{% endif %}" />
|
||||||
|
</svg>
|
||||||
|
</button>
|
Loading…
x
Reference in New Issue
Block a user