feat: redesign availability input

This commit is contained in:
Max Hohlfeld 2025-05-07 19:52:42 +02:00
parent 8b61bb37a8
commit a6b12d9bf2
7 changed files with 173 additions and 105 deletions

View File

@ -1,12 +1,10 @@
use actix_web::{web, HttpResponse, Responder};
use chrono::NaiveDate;
use chrono::{Days, NaiveDate, NaiveTime};
use serde::Deserialize;
use sqlx::PgPool;
use crate::endpoints::availability::NewOrEditAvailabilityTemplate;
use crate::models::{
find_free_date_time_slots, Availability, User,
};
use crate::models::{find_free_date_time_slots, Availability, User};
use crate::utils::{ApplicationError, TemplateResponse};
#[derive(Deserialize)]
@ -24,8 +22,8 @@ pub async fn get(
Availability::read_by_user_and_date(pool.get_ref(), user.id, &query.date).await?;
let slot_suggestions = find_free_date_time_slots(&availabilities_from_user);
let user_can_create_availabillity = availabilities_from_user.is_empty()
|| !slot_suggestions.is_empty();
let user_can_create_availabillity =
availabilities_from_user.is_empty() || !slot_suggestions.is_empty();
if !user_can_create_availabillity {
return Ok(HttpResponse::BadRequest().finish());
@ -34,11 +32,13 @@ pub async fn get(
let template = NewOrEditAvailabilityTemplate {
user: user.into_inner(),
date: query.date,
enddate: None,
id: None,
start: None,
end: 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(),
};
Ok(template.to_response()?)

View File

@ -1,4 +1,5 @@
use actix_web::{web, HttpResponse, Responder};
use chrono::Days;
use sqlx::PgPool;
use crate::{
@ -33,11 +34,17 @@ pub async fn get(
let template = NewOrEditAvailabilityTemplate {
user: user.into_inner(),
date: availability.start.date(),
enddate: Some(availability.end.date()),
id: Some(path.id),
start: Some(availability.start.time()),
end: Some(availability.end),
end: Some(availability.end.time()),
comment: availability.comment.as_deref(),
slot_suggestions
slot_suggestions,
datetomorrow: availability
.start
.date()
.checked_add_days(Days::new(1))
.unwrap(),
};
Ok(template.to_response()?)

View File

@ -1,5 +1,7 @@
use crate::filters;
use askama::Template;
use chrono::{Days, NaiveDate, NaiveDateTime, NaiveTime};
use chrono::{NaiveDate, NaiveDateTime, NaiveTime};
use serde::Deserialize;
use crate::models::{Availability, AvailabilityChangeset, Role, User};
@ -15,11 +17,22 @@ pub mod post_update;
struct NewOrEditAvailabilityTemplate<'a> {
user: User,
date: NaiveDate,
enddate: Option<NaiveDate>,
id: Option<i32>,
start: Option<NaiveTime>,
end: Option<NaiveDateTime>,
end: Option<NaiveTime>,
comment: Option<&'a str>,
slot_suggestions: Vec<(NaiveDateTime, NaiveDateTime)>,
datetomorrow: NaiveDate
}
#[derive(Deserialize)]
pub struct AvailabillityForm {
pub startdate: NaiveDate,
pub enddate: NaiveDate,
pub starttime: NaiveTime,
pub endtime: NaiveTime,
pub comment: Option<String>,
}
fn find_adjacend_availability<'a>(

View File

@ -1,23 +1,13 @@
use actix_web::{http::header::LOCATION, web, HttpResponse, Responder};
use chrono::{NaiveDate, NaiveDateTime, NaiveTime};
use garde::Validate;
use serde::Deserialize;
use sqlx::PgPool;
use crate::{
endpoints::availability::find_adjacend_availability,
endpoints::availability::{find_adjacend_availability, AvailabillityForm},
models::{Availability, AvailabilityChangeset, AvailabilityContext, User},
utils::{self, ApplicationError},
};
#[derive(Deserialize)]
pub struct AvailabillityForm {
pub date: NaiveDate,
pub from: NaiveTime,
pub till: NaiveDateTime,
pub comment: Option<String>,
}
#[actix_web::post("/availabillity/new")]
pub async fn post(
user: web::ReqData<User>,
@ -25,13 +15,16 @@ pub async fn post(
form: web::Form<AvailabillityForm>,
) -> Result<impl Responder, ApplicationError> {
let existing_availabilities =
Availability::read_by_user_and_date(pool.get_ref(), user.id, &form.date).await?;
Availability::read_by_user_and_date(pool.get_ref(), user.id, &form.startdate).await?;
let context = AvailabilityContext {
existing_availabilities: existing_availabilities.clone(),
};
let start = form.startdate.and_time(form.starttime);
let end = form.enddate.and_time(form.endtime);
let mut changeset = AvailabilityChangeset {
time: (form.date.and_time(form.from), form.till),
time: (start, end),
comment: form.comment.clone(),
};
@ -55,7 +48,7 @@ pub async fn post(
Availability::create(pool.get_ref(), user.id, changeset).await?;
}
let url = utils::get_return_url_for_date(&form.date);
let url = utils::get_return_url_for_date(&form.startdate);
Ok(HttpResponse::Found()
.insert_header((LOCATION, url.clone()))
.insert_header(("HX-LOCATION", url))

View File

@ -4,7 +4,7 @@ use sqlx::PgPool;
use crate::{
endpoints::{
availability::{find_adjacend_availability, post_new::AvailabillityForm},
availability::{find_adjacend_availability, AvailabillityForm},
IdPath,
},
models::{Availability, AvailabilityChangeset, AvailabilityContext, User},
@ -37,8 +37,11 @@ pub async fn post(
existing_availabilities: existing_availabilities.clone(),
};
let start = form.startdate.and_time(form.starttime);
let end = form.enddate.and_time(form.endtime);
let mut changeset = AvailabilityChangeset {
time: (form.date.and_time(form.from), form.till),
time: (start, end),
comment: form.comment.clone(),
};
@ -65,7 +68,7 @@ pub async fn post(
Availability::update(pool.get_ref(), availability.id, changeset).await?;
}
let url = utils::get_return_url_for_date(&form.date);
let url = utils::get_return_url_for_date(&form.startdate);
Ok(HttpResponse::Found()
.insert_header((LOCATION, url.clone()))
.insert_header(("HX-LOCATION", url))

View File

@ -1,4 +1,6 @@
use chrono::{NaiveDate, NaiveDateTime};
use std::fmt::Display;
use chrono::{NaiveDate, NaiveDateTime, NaiveTime};
use maud::html;
use crate::models::UserFunction;
@ -21,7 +23,10 @@ pub fn cond_show(show: &bool, text: &str) -> askama::Result<String> {
}
}
pub fn insert_value(option: &Option<String>) -> askama::Result<String> {
pub fn insert_value<T>(option: &Option<T>) -> askama::Result<String>
where
T: Display,
{
if let Some(val) = option {
let s = format!(r#"value="{val}""#);
return Ok(s);
@ -30,6 +35,15 @@ pub fn insert_value(option: &Option<String>) -> askama::Result<String> {
Ok(String::new())
}
pub fn insert_time_value(option: &Option<NaiveTime>) -> askama::Result<String> {
if let Some(val) = option {
let s = val.format(r#"value="%H:%M""#).to_string();
return Ok(s);
}
Ok(String::new())
}
pub fn is_some_and_eq<T>(option: &Option<T>, other: &T) -> askama::Result<bool>
where
T: Eq,
@ -71,6 +85,22 @@ pub fn date_d(v: &NaiveDate) -> askama::Result<String> {
Ok(v.format("%d.%m.%Y").to_string())
}
pub fn date_c(v: &NaiveDate) -> askama::Result<String> {
Ok(v.format("%d.%m").to_string())
}
pub fn time(v: &NaiveTime) -> askama::Result<String> {
Ok(v.format("%H:%M").to_string())
}
pub fn time_opt(v: &Option<NaiveTime>, default: &str) -> askama::Result<String> {
if let Some(t) = v {
return time(t);
}
Ok(default.to_string())
}
pub fn dt_t(v: &NaiveDateTime) -> askama::Result<String> {
Ok(v.format("%R").to_string())
}

View File

@ -3,87 +3,109 @@
{% block content %}
<section class="section">
<div class="container">
{% if id.is_some() %}
<form method="post" action="/availabillity/edit/{{ id.unwrap() }}">
<h1 class="title">Bearbeite Vefügbarkeit für den {{ date.format("%d.%m.%Y") }}</h1>
{% else %}
<form method="post" action="/availabillity/new">
<h1 class="title">Neue Vefügbarkeit für den {{ date.format("%d.%m.%Y") }}</h1>
{% endif %}
{% let is_edit = id.is_some() %}
<form method="post" action="/availabillity/{% if is_edit %}edit/{{ id.unwrap() }}{% else %}new{% endif %}">
<h1 class="title">{% if is_edit %}Bearbeite{% else %}Neue{% endif %} Vefügbarkeit für den {{ date|date_d }}</h1>
<input type="hidden" name="date" value="{{ date }}">
{% let time = "%R" %}
<input type="hidden" name="startdate" value="{{ date }}">
<input type="hidden" name="enddate" value="{{ date }}" id="enddate">
<div class="field is-horizontal">
<div class="field-label">
<label class="label">Dauer Von - Bis</label>
</div>
<div class="field-body">
<div class="field">
<input class="input" type="time" id="from" name="from" required {% if let Some(start)=start
%}value="{{start}}" {% endif %}>
{% if slot_suggestions.len() > 0 %}
<p class="help">noch mögliche Zeiträume:</p>
<div class="tags help">
{% for (s, e) in slot_suggestions %}
<span class="tag">{{ s.format(time) }} - {{ e.format(time) }}</span>
{% endfor %}
</div>
{% endif %}
<div class="field is-horizontal">
<div class="field-label">
<label class="label">Dauer Von - Bis</label>
</div>
<div class="field-body">
<div class="field">
<input class="input" type="time" name="starttime" required {{ start|insert_time_value|safe }}
_="on change put the value of me into #st">
{% if slot_suggestions.len() > 0 %}
<p class="help">noch mögliche Zeiträume:</p>
<div class="tags help">
{% for (s, e) in slot_suggestions %}
<span class="tag">{{ *s|dt_t }} - {{ *e|dt_t }}</span>
{% endfor %}
</div>
<div class="field">
<input class="input" type="datetime-local" id="till" name="till" required min="{{ date }}T00:00"
max="{{ date.checked_add_days(Days::new(1)).unwrap() }}T23:59" {% if let Some(end)=end %}value="{{end}}"
{% endif %}>
{% endif %}
</div>
<div class="field">
<input class="input" type="time" name="endtime" required {{ end|insert_time_value|safe }}
_='on change put the value of me into #et then if (value of the previous <input/>) is greater than (value of me) then set the value of #enddate to "{{ datetomorrow }}" then add @disabled to #nextbtn then remove @disabled from #samebtn then put "{{ datetomorrow|date_c }}" into #ed end' />
</div>
</div>
</div>
<div class="field is-horizontal">
<div class="field-label">
<label class="label">Verfügbarkeitsende</label>
</div>
<div class="field-body">
<div class="field is-narrow">
{% let is_overnight = enddate.is_some() && enddate.as_ref().unwrap() == datetomorrow|ref %}
<button id="samebtn" class="button is-small is-success is-light" type="button"
{{ is_overnight|invert|ref|cond_show("disabled")|safe }}
_='on click set the value of #enddate to "{{ date }}" then toggle @disabled on me then toggle @disabled on #nextbtn then put "{{ date|date_c }}" into #ed'>am
selben Tag</button>
</div>
<div class="field is-narrow">
<button id="nextbtn" class="button is-small is-info is-light" type="button"
{{ is_overnight|cond_show("disabled")|safe }}
_='on click set the value of #enddate to "{{ datetomorrow }}" then toggle @disabled on me then toggle @disabled on #samebtn then put "{{ datetomorrow|date_c }}" into #ed'>am
Tag darauf</button>
</div>
</div>
</div>
<div class="field is-horizontal">
<div class="field-label">
<label class="label">Kommentar</label>
</div>
<div class="field-body">
<div class="field">
<div class="control">
<textarea class="textarea" name="comment" placeholder="nur Posten, nur Wachhabender, etc..">{{
comment.unwrap_or("") }}</textarea>
</div>
<p class="help is-info">
verfügbar von {{ date|date_c }} <span id="st">{{ start|time_opt("10:00")|safe }}</span> Uhr
bis <span id="ed">{{ enddate.as_ref().unwrap_or(date)|date_c }}</span>
<span id="et">{{ end|time_opt("20:00")|safe }}</span> Uhr
</p>
</div>
</div>
</div>
<div class="field is-horizontal">
<div class="field-label"></div>
<div class="field-body">
<div class="field is-grouped">
<div class="control">
<button class="button is-success">
<svg class="icon">
<use href="/static/feather-sprite.svg#check-circle" />
</svg>
<span>
{% if id.is_some() %}
Speichern
{% else %}
Erstellen
{% endif %}
</span>
</button>
</div>
<div class="control">
<a class="button is-link is-light" hx-boost="true" href="/?date={{ date }}">
<svg class="icon">
<use href="/static/feather-sprite.svg#arrow-left" />
</svg>
<span>Zurück</span>
</a>
</div>
</div>
</div>
</div>
<div class="field is-horizontal">
<div class="field-label">
<label class="label">Kommentar</label>
</div>
<div class="field-body">
<div class="field">
<div class="control">
<textarea class="textarea" name="comment" placeholder="nur Posten, nur Wachhabender, etc..">{{
comment.unwrap_or("") }}</textarea>
</div>
</div>
</div>
</div>
<div class="field is-horizontal">
<div class="field-label"></div>
<div class="field-body">
<div class="field is-grouped">
<div class="control">
<button class="button is-success">
<svg class="icon">
<use href="/static/feather-sprite.svg#check-circle" />
</svg>
<span>
{% if id.is_some() %}
Speichern
{% else %}
Erstellen
{% endif %}
</span>
</button>
</div>
<div class="control">
<a class="button is-link is-light" hx-boost="true" href="/?date={{ date }}">
<svg class="icon">
<use href="/static/feather-sprite.svg#arrow-left" />
</svg>
<span>Zurück</span>
</a>
</div>
</div>
</div>
</div>
</form>
</form>
</div>
</section>
{% endblock %}