Compare commits
3 Commits
1d493b85b1
...
66dd99dd7c
Author | SHA1 | Date | |
---|---|---|---|
66dd99dd7c | |||
d34f55471b | |||
c2cd1f9c85 |
@ -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);
|
||||
}
|
||||
|
||||
|
@ -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()?)
|
||||
|
64
web/src/endpoints/availability/get_new_other.rs
Normal file
64
web/src/endpoints/availability/get_new_other.rs
Normal 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()?)
|
||||
}
|
@ -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()?)
|
||||
|
@ -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>
|
||||
}
|
||||
|
@ -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);
|
||||
|
@ -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?
|
||||
|
@ -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);
|
||||
|
@ -3,7 +3,7 @@ $crimson: #00d1b2; //#B80F0A;
|
||||
|
||||
// Override global Sass variables from the /utilities folder
|
||||
@use "bulma/sass/utilities" with ($family-primary: '"Nunito", sans-serif',
|
||||
$primary: $crimson,
|
||||
$primary: $crimson,
|
||||
);
|
||||
// $grey-dark: $brown,
|
||||
// $grey-light: $beige-light,
|
||||
@ -82,9 +82,9 @@ section.htmx-request {
|
||||
}
|
||||
|
||||
a.dropdown-item[disabled] {
|
||||
color: hsl(221, 14%, 48%); // $grey;
|
||||
cursor: default;
|
||||
pointer-events: none;
|
||||
color: hsl(221, 14%, 48%); // $grey;
|
||||
cursor: default;
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
#toast {
|
||||
@ -100,16 +100,47 @@ a.dropdown-item[disabled] {
|
||||
}
|
||||
|
||||
#toast-progress {
|
||||
height: 5px;
|
||||
position: relative;
|
||||
animation: progressBar 3s ease-in-out;
|
||||
animation-iteration-count: infinite;
|
||||
animation-fill-mode:both;
|
||||
border-radius: 20px 20px 0 0;
|
||||
height: 5px;
|
||||
position: relative;
|
||||
animation: progressBar 3s ease-in-out;
|
||||
animation-iteration-count: infinite;
|
||||
animation-fill-mode: both;
|
||||
border-radius: 20px 20px 0 0;
|
||||
}
|
||||
|
||||
@keyframes progressBar {
|
||||
0% { width: 0; }
|
||||
100% { width: 100%; }
|
||||
0% {
|
||||
width: 0;
|
||||
}
|
||||
|
||||
100% {
|
||||
width: 100%;
|
||||
}
|
||||
}
|
||||
|
||||
// took from https://github.com/auth0/kbd
|
||||
kbd {
|
||||
font-family: Consolas, "Lucida Console", monospace;
|
||||
display: inline-block;
|
||||
border-radius: 3px;
|
||||
padding: 0px 4px;
|
||||
box-shadow: 1px 1px 1px #777;
|
||||
margin: 2px;
|
||||
font-size: small;
|
||||
vertical-align: text-bottom;
|
||||
background: #eee;
|
||||
font-weight: 500;
|
||||
color: #555;
|
||||
font-variant: small-caps;
|
||||
font-weight: 600;
|
||||
|
||||
letter-spacing: 1px;
|
||||
|
||||
/* Prevent selection */
|
||||
-webkit-touch-callout: none;
|
||||
-webkit-user-select: none;
|
||||
-khtml-user-select: none;
|
||||
-moz-user-select: none;
|
||||
-ms-user-select: none;
|
||||
user-select: none;
|
||||
}
|
||||
|
@ -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>
|
||||
|
@ -18,7 +18,9 @@
|
||||
|
||||
<div class="control level-item is-flex-grow-0">
|
||||
<input class="input" type="date" name="date" value="{{ date }}" hx-target="closest body"
|
||||
hx-get="/calendar{{ selected_area|show_area_query(true) }}" hx-push-url="true">
|
||||
hx-get="/calendar{{ selected_area|show_area_query(true) }}" hx-push-url="true"
|
||||
_="on keyup[key is 'D'] from elsewhere showPicker() on me end
|
||||
on keyup[key is 'S'] from elsewhere focus() on me">
|
||||
|
||||
{% if user.role == Role::Admin %}
|
||||
<div class="select ml-2">
|
||||
@ -34,6 +36,22 @@
|
||||
</select>
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
<button class="button ml-2" title="Bedieninformationen"
|
||||
_="on click call Swal.fire({
|
||||
title: 'Bedieninformationen',
|
||||
icon: 'info',
|
||||
showCloseButton: true,
|
||||
showConfirmButton: false,
|
||||
html: '<kbd>S</kbd> Datumsfeld zur Texteingabe fokussieren<br/>
|
||||
<kbd>D</kbd> Datumsauswahlfenster einblenden<br/>
|
||||
<kbd>←</kbd> zum vorherigen Tag navigieren<br/>
|
||||
<kbd>→</kbd> zum nächsten Tag navigieren'
|
||||
})">
|
||||
<svg class="icon">
|
||||
<use href="/static/feather-sprite.svg#info" />
|
||||
</svg>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div class="level-right">
|
||||
@ -100,9 +118,10 @@
|
||||
<div class="container">
|
||||
<div class="level">
|
||||
<div class="level-left">
|
||||
<h3 class="title is-3">
|
||||
Events am {{ date|fmt_date(WeekdayDayMonthYear) }}
|
||||
</h3>
|
||||
<div class="level-item is-flex-direction-column is-align-items-start">
|
||||
<h3 class="title is-3">Veranstaltungen</h3>
|
||||
<h5 class="subtitle is-5">am {{ date|fmt_date(WeekdayDayMonthYear) }}</h5>
|
||||
</div>
|
||||
</div>
|
||||
{% if user.role == Role::Admin || user.role == Role::AreaManager && (selected_area.is_none() ||
|
||||
selected_area.unwrap() == user.area_id) %}
|
||||
@ -111,15 +130,15 @@
|
||||
<svg class="icon">
|
||||
<use href="/static/feather-sprite.svg#plus-circle" />
|
||||
</svg>
|
||||
<span>Neues Event für diesen Tag</span>
|
||||
<span>Neue Veranstaltung für diesen Tag</span>
|
||||
</a>
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
|
||||
{% if events_and_assignments.len() == 0 %}
|
||||
<div class="box">
|
||||
<h5 class="subtitle is-5">keine Events geplant</h5>
|
||||
<div class="notification">
|
||||
Keine Veranstaltungen geplant.
|
||||
</div>
|
||||
{% else %}
|
||||
{% for (event, posten, fuehrungsassistent, wachhabender, vehicle) in events_and_assignments %}
|
||||
@ -229,27 +248,34 @@
|
||||
<div class="container">
|
||||
<div class="level">
|
||||
<div class="level-left">
|
||||
<h3 class="title is-3">
|
||||
Verfügbarkeiten am {{ date|fmt_date(WeekdayDayMonthYear) }}
|
||||
</h3>
|
||||
<div class="level-item is-flex-direction-column is-align-items-start">
|
||||
<h3 class="title is-3">Verfügbarkeiten</h3>
|
||||
<h5 class="subtitle is-5">am {{ date|fmt_date(WeekdayDayMonthYear) }}</h5>
|
||||
</div>
|
||||
</div>
|
||||
{% if selected_area.is_none() || selected_area.unwrap() == user.area_id %}
|
||||
<div class="level-right">
|
||||
<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>
|
||||
{% let btn_disabled = !user_can_create_availability %}
|
||||
<button class="button is-link is-light" hx-get="/availability/new?date={{ date }}" {{
|
||||
btn_disabled|cond_show("disabled") }} hx-target="closest body">
|
||||
<a class="button is-link is-light" href="/availability/new?date={{ date }}" {{
|
||||
btn_disabled|cond_show("disabled") }}>
|
||||
<svg class="icon">
|
||||
<use href="/static/feather-sprite.svg#plus-circle" />
|
||||
</svg>
|
||||
<span>Neue Verfügbarkeit für diesen Tag</span>
|
||||
</button>
|
||||
</a>
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
|
||||
{% if availabilities.len() == 0 %}
|
||||
<div class="box">
|
||||
<h5 class="subtitle is-5">keine Verfügbarkeiten eingetragen</h5>
|
||||
<div class="notification">
|
||||
Keine Verfügbarkeiten hinterlegt.
|
||||
</div>
|
||||
{% else %}
|
||||
<div class="box">
|
||||
@ -279,21 +305,19 @@
|
||||
{{ 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 }}">
|
||||
<a class="button is-primary is-light" href="/availability/edit/{{ availability.id }}"
|
||||
title="Verfügbarkeit bearbeiten">
|
||||
<svg class="icon">
|
||||
<use href="/static/feather-sprite.svg#edit" />
|
||||
</svg>
|
||||
<span>Bearbeiten</span>
|
||||
</a>
|
||||
<button class="button is-danger is-light" hx-delete="/availability/delete/{{ availability.id }}"
|
||||
hx-target="closest tr" hx-swap="delete" hx-trigger="confirmed">
|
||||
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>
|
||||
<span>Löschen</span>
|
||||
</button>
|
||||
</div>
|
||||
{% endif %}
|
||||
|
Loading…
x
Reference in New Issue
Block a user