feat: add, edit, delete foreign availabilities as admin or areamanger

This commit is contained in:
Max Hohlfeld 2025-07-19 22:07:23 +02:00
parent 1d493b85b1
commit c2cd1f9c85
10 changed files with 152 additions and 27 deletions

View File

@ -2,7 +2,7 @@ use actix_web::{web, HttpResponse, Responder};
use sqlx::PgPool;
use crate::{endpoints::IdPath, utils::ApplicationError};
use brass_db::models::{Availability, User};
use brass_db::models::{Availability, Role, User};
#[actix_web::delete("/availability/delete/{id}")]
pub async fn delete(
@ -14,7 +14,8 @@ pub async fn delete(
return Ok(HttpResponse::NotFound().finish());
};
if availability.user_id != user.id {
if user.role != Role::Admin && user.role != Role::AreaManager && availability.user_id != user.id
{
return Err(ApplicationError::Unauthorized);
}

View File

@ -1,24 +1,18 @@
use actix_web::{web, HttpResponse, Responder};
use chrono::{Days, NaiveDate, NaiveTime};
use serde::Deserialize;
use chrono::{Days, NaiveTime};
use sqlx::PgPool;
use crate::{
endpoints::availability::NewOrEditAvailabilityTemplate,
endpoints::{availability::NewOrEditAvailabilityTemplate, NaiveDateQuery},
utils::{ApplicationError, TemplateResponse},
};
use brass_db::models::{find_free_date_time_slots, Availability, User};
#[derive(Deserialize)]
struct AvailabilityNewQuery {
date: NaiveDate,
}
#[actix_web::get("/availability/new")]
pub async fn get(
user: web::ReqData<User>,
pool: web::Data<PgPool>,
query: web::Query<AvailabilityNewQuery>,
query: web::Query<NaiveDateQuery>,
) -> Result<impl Responder, ApplicationError> {
let availabilities_from_user =
Availability::read_all_by_user_and_date(pool.get_ref(), user.id, &query.date).await?;
@ -41,6 +35,8 @@ pub async fn get(
comment: None,
slot_suggestions,
datetomorrow: query.date.checked_add_days(Days::new(1)).unwrap(),
other_user: None,
other_users: Vec::default(),
};
Ok(template.to_response()?)

View File

@ -0,0 +1,64 @@
use actix_web::{web, Responder};
use brass_db::models::{find_free_date_time_slots, Availability, Role, User};
use chrono::{Days, NaiveDate, NaiveTime};
use serde::Deserialize;
use sqlx::PgPool;
use crate::{
endpoints::availability::NewOrEditAvailabilityTemplate,
utils::{ApplicationError, TemplateResponse},
};
#[derive(Deserialize)]
struct AvailabilityNewOtherQuery {
date: NaiveDate,
user: Option<i32>,
}
#[actix_web::get("/availability/new-other")]
pub async fn get(
user: web::ReqData<User>,
pool: web::Data<PgPool>,
query: web::Query<AvailabilityNewOtherQuery>,
) -> Result<impl Responder, ApplicationError> {
if user.role != Role::Admin && user.role != Role::AreaManager {
return Err(ApplicationError::Unauthorized);
}
let other_user = if let Some(id) = query.user {
User::read_by_id(pool.get_ref(), id).await?.map(|u| u.id)
} else {
None
};
let slot_suggestions = if let Some(id) = other_user {
let availabilities =
Availability::read_all_by_user_and_date(pool.get_ref(), id, &query.date).await?;
find_free_date_time_slots(&availabilities)
} else {
Vec::default()
};
let mut other_users = if user.role == Role::AreaManager {
User::read_all_by_area(pool.get_ref(), user.area_id).await?
} else {
User::read_all_including_area(pool.get_ref()).await?
};
other_users.retain(|u| u.id != user.id);
let template = NewOrEditAvailabilityTemplate {
user: user.into_inner(),
date: query.date,
enddate: None,
id: None,
start: Some(NaiveTime::from_hms_opt(10, 0, 0).unwrap()),
end: Some(NaiveTime::from_hms_opt(20, 0, 0).unwrap()),
comment: None,
slot_suggestions,
datetomorrow: query.date.checked_add_days(Days::new(1)).unwrap(),
other_user,
other_users,
};
Ok(template.to_response()?)
}

View File

@ -6,7 +6,7 @@ use crate::{
endpoints::{availability::NewOrEditAvailabilityTemplate, IdPath},
utils::{ApplicationError, TemplateResponse},
};
use brass_db::models::{find_free_date_time_slots, Availability, User};
use brass_db::models::{find_free_date_time_slots, Availability, Role, User};
#[actix_web::get("/availability/edit/{id}")]
pub async fn get(
@ -18,19 +18,37 @@ pub async fn get(
return Ok(HttpResponse::NotFound().finish());
};
if availability.user_id != user.id {
if user.role != Role::Admin && user.role != Role::AreaManager && availability.user_id != user.id
{
return Err(ApplicationError::Unauthorized);
}
let availabilities =
Availability::read_all_by_user_and_date(pool.get_ref(), user.id, &availability.start.date())
.await?;
let availabilities = Availability::read_all_by_user_and_date(
pool.get_ref(),
availability.user_id,
&availability.start.date(),
)
.await?;
let slot_suggestions = find_free_date_time_slots(&availabilities)
.into_iter()
.filter(|(a, b)| *b == availability.start || *a == availability.end)
.collect();
let other_user = if availability.user_id != user.id {
Some(availability.user_id)
} else {
None
};
let other_users = if availability.user_id != user.id {
vec![User::read_by_id(pool.get_ref(), availability.user_id)
.await?
.unwrap()]
} else {
Vec::default()
};
let template = NewOrEditAvailabilityTemplate {
user: user.into_inner(),
date: availability.start.date(),
@ -45,6 +63,8 @@ pub async fn get(
.date()
.checked_add_days(Days::new(1))
.unwrap(),
other_user,
other_users,
};
Ok(template.to_response()?)

View File

@ -11,6 +11,7 @@ use brass_db::models::{Role, User};
pub mod delete;
pub mod get_calendar;
pub mod get_new;
pub mod get_new_other;
pub mod get_overview;
pub mod get_update;
pub mod post_new;
@ -28,6 +29,8 @@ struct NewOrEditAvailabilityTemplate<'a> {
comment: Option<&'a str>,
slot_suggestions: Vec<(NaiveDateTime, NaiveDateTime)>,
datetomorrow: NaiveDate,
other_user: Option<i32>,
other_users: Vec<User>
}
#[derive(Deserialize)]
@ -37,4 +40,5 @@ pub struct AvailabilityForm {
pub starttime: NaiveTime,
pub endtime: NaiveTime,
pub comment: Option<String>,
pub user: Option<i32>
}

View File

@ -19,10 +19,11 @@ pub async fn post(
) -> Result<impl Responder, ApplicationError> {
let start = form.startdate.and_time(form.starttime);
let end = form.enddate.and_time(form.endtime);
let user_for_availability = form.user.unwrap_or(user.id);
let context = AvailabilityContext {
pool: pool.get_ref(),
user_id: user.id,
user_id: user_for_availability,
availability: None,
};
@ -46,9 +47,14 @@ pub async fn post(
return Ok(HttpResponse::UnprocessableEntity().body(error_message.into_string()));
};
if let Some(a) =
Availability::find_adjacent_by_time_for_user(pool.get_ref(), &start, &end, user.id, None)
.await?
if let Some(a) = Availability::find_adjacent_by_time_for_user(
pool.get_ref(),
&start,
&end,
user_for_availability,
None,
)
.await?
{
if a.end == start {
changeset.time.0 = a.start;
@ -60,7 +66,7 @@ pub async fn post(
Availability::update(pool.get_ref(), a.id, changeset).await?;
} else {
Availability::create(pool.get_ref(), user.id, changeset).await?;
Availability::create(pool.get_ref(), user_for_availability, changeset).await?;
}
let url = utils::get_return_url_for_date(&form.startdate);

View File

@ -7,7 +7,7 @@ use crate::{
utils::{self, ApplicationError},
};
use brass_db::{
models::{Availability, AvailabilityChangeset, AvailabilityContext, User},
models::{Availability, AvailabilityChangeset, AvailabilityContext, Role, User},
validation::AsyncValidate,
};
@ -22,12 +22,14 @@ pub async fn post(
return Ok(HttpResponse::NotFound().finish());
};
if availability.user_id != user.id {
if user.role != Role::Admin && user.role != Role::AreaManager && availability.user_id != user.id
{
return Err(ApplicationError::Unauthorized);
}
let start = form.startdate.and_time(form.starttime);
let end = form.enddate.and_time(form.endtime);
let user_for_availability = form.user.unwrap_or(user.id);
let comment = form
.comment
@ -36,7 +38,7 @@ pub async fn post(
let context = AvailabilityContext {
pool: pool.get_ref(),
user_id: user.id,
user_id: user_for_availability,
availability: Some(availability.id),
};
@ -59,7 +61,7 @@ pub async fn post(
pool.get_ref(),
&start,
&end,
user.id,
user_for_availability,
Some(availability.id),
)
.await?

View File

@ -10,7 +10,7 @@ mod events;
mod export;
mod imprint;
mod location;
pub mod user; // TODO: why pub?
pub mod user;
mod vehicle;
mod vehicle_assignment;
@ -56,6 +56,7 @@ pub fn init(cfg: &mut ServiceConfig) {
cfg.service(availability::delete::delete);
cfg.service(availability::get_new::get);
cfg.service(availability::get_new_other::get);
cfg.service(availability::get_calendar::get);
cfg.service(availability::get_update::get);
cfg.service(availability::post_new::post);

View File

@ -12,6 +12,31 @@
<input type="hidden" name="startdate" value="{{ date }}">
<input type="hidden" name="enddate" value="{{ enddate.as_ref().unwrap_or(date) }}" id="enddate">
{% if other_users.len() != 0 %}
<div class="field is-horizontal">
<div class="field-label">
<label class="label">Nutzer</label>
</div>
<div class="field-body">
<div class="field is-narrow">
<div class="control">
<div class="select is-fullwidth">
<select name="user" required {{ id.is_some()|ref|cond_show("disabled") }}>
{% for u in other_users %}
<option value="{{ u.id }}" hx-get="/availability/new-other?date={{ date }}&user={{ u.id }}"
{{ (other_user.is_some() && *other_user.as_ref().unwrap()==u.id)|cond_show("selected") }}>
{{ u.name }}
{% if user.role == Role::Admin && u.area.is_some() %} - ({{ u.area.as_ref().unwrap().name }}){% endif %}
</option>
{% endfor %}
</select>
</div>
</div>
</div>
</div>
</div>
{% endif %}
<div class="field is-horizontal">
<div class="field-label">
<label class="label">Dauer Von - Bis</label>

View File

@ -236,6 +236,12 @@
{% if selected_area.is_none() || selected_area.unwrap() == user.area_id %}
<div class="level-right">
{% let btn_disabled = !user_can_create_availability %}
<a class="button is-link is-outlined" href="/availability/new-other?date={{ date }}">
<svg class="icon">
<use href="/static/feather-sprite.svg#plus-circle" />
</svg>
<span>Neue Verfügbarkeit für anderen Nutzer</span>
</a>
<button class="button is-link is-light" hx-get="/availability/new?date={{ date }}" {{
btn_disabled|cond_show("disabled") }} hx-target="closest body">
<svg class="icon">
@ -279,7 +285,7 @@
{{ availability.comment.as_deref().unwrap_or("") }}
</td>
<td>
{% if availability.user_id == user.id %}
{% if availability.user_id == user.id || user.role == Role::Admin || user.role == Role::AreaManager %}
<div class="buttons is-right">
<a class="button is-primary is-light" hx-boost="true"
href="/availability/edit/{{ availability.id }}">