feat: toggle cross areal

This commit is contained in:
Max Hohlfeld 2025-07-24 16:25:11 +02:00
parent 5063933f33
commit 290e610058
7 changed files with 168 additions and 4 deletions

View 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"
}

View File

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

View File

@ -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<i32>,
other_users: Vec<User>
other_users: Vec<User>,
}
#[derive(Deserialize)]
@ -40,5 +41,5 @@ pub struct AvailabilityForm {
pub starttime: NaiveTime,
pub endtime: NaiveTime,
pub comment: Option<String>,
pub user: Option<i32>
pub user: Option<i32>,
}

View 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());
// }
}

View File

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

View File

@ -312,7 +312,7 @@
{{ availability.end|fmt_datetime(DayMonthYearHourMinute) }} Uhr
</td>
<td>
{{ availability.comment.as_deref().unwrap_or("") }}
{{ availability.comment.as_deref().unwrap_or_default() }}
</td>
<td>
{% if availability.user_id == user.id || user.role == Role::Admin || user.role == Role::AreaManager %}
@ -324,11 +324,16 @@
</svg>
</a>
<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">
<use href="/static/feather-sprite.svg#x-circle" />
</svg>
</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>
{% endif %}
</td>

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