Compare commits

...

3 Commits

17 changed files with 532 additions and 156 deletions

View File

@ -0,0 +1,114 @@
{
"db_name": "PostgreSQL",
"query": "\n SELECT\n event.id AS eventId,\n event.startTimestamp,\n event.endTimestamp,\n event.name,\n event.locationId,\n event.voluntaryWachhabender,\n event.voluntaryFuehrungsassistent,\n event.amountOfPosten,\n event.clothing,\n event.canceled,\n event.note,\n location.id,\n location.name AS locationName,\n location.areaId AS locationAreaId,\n clothing.id AS clothingId,\n clothing.name AS clothingName\n FROM event\n JOIN location ON event.locationId = location.id\n JOIN clothing ON event.clothing = clothing.id\n WHERE starttimestamp::date >= $1\n AND starttimestamp::date <= $2\n AND location.areaId = $3;\n ",
"describe": {
"columns": [
{
"ordinal": 0,
"name": "eventid",
"type_info": "Int4"
},
{
"ordinal": 1,
"name": "starttimestamp",
"type_info": "Timestamptz"
},
{
"ordinal": 2,
"name": "endtimestamp",
"type_info": "Timestamptz"
},
{
"ordinal": 3,
"name": "name",
"type_info": "Text"
},
{
"ordinal": 4,
"name": "locationid",
"type_info": "Int4"
},
{
"ordinal": 5,
"name": "voluntarywachhabender",
"type_info": "Bool"
},
{
"ordinal": 6,
"name": "voluntaryfuehrungsassistent",
"type_info": "Bool"
},
{
"ordinal": 7,
"name": "amountofposten",
"type_info": "Int2"
},
{
"ordinal": 8,
"name": "clothing",
"type_info": "Int4"
},
{
"ordinal": 9,
"name": "canceled",
"type_info": "Bool"
},
{
"ordinal": 10,
"name": "note",
"type_info": "Text"
},
{
"ordinal": 11,
"name": "id",
"type_info": "Int4"
},
{
"ordinal": 12,
"name": "locationname",
"type_info": "Text"
},
{
"ordinal": 13,
"name": "locationareaid",
"type_info": "Int4"
},
{
"ordinal": 14,
"name": "clothingid",
"type_info": "Int4"
},
{
"ordinal": 15,
"name": "clothingname",
"type_info": "Text"
}
],
"parameters": {
"Left": [
"Date",
"Date",
"Int4"
]
},
"nullable": [
false,
false,
false,
false,
false,
false,
false,
false,
false,
false,
true,
false,
false,
false,
false,
false
]
},
"hash": "10b4b80f351b66ac5e778a3031288ac5dc66efd0a66b38b7e30f4c954df91bdf"
}

View File

@ -0,0 +1,48 @@
{
"db_name": "PostgreSQL",
"query": "\n SELECT\n availability.id,\n availability.userId,\n availability.startTimestamp,\n availability.endTimestamp,\n availability.comment\n FROM availability\n WHERE availability.userId = $1\n AND availability.starttimestamp::date >= $2\n AND availability.endtimestamp::date <= $3;\n ",
"describe": {
"columns": [
{
"ordinal": 0,
"name": "id",
"type_info": "Int4"
},
{
"ordinal": 1,
"name": "userid",
"type_info": "Int4"
},
{
"ordinal": 2,
"name": "starttimestamp",
"type_info": "Timestamptz"
},
{
"ordinal": 3,
"name": "endtimestamp",
"type_info": "Timestamptz"
},
{
"ordinal": 4,
"name": "comment",
"type_info": "Text"
}
],
"parameters": {
"Left": [
"Int4",
"Date",
"Date"
]
},
"nullable": [
false,
false,
false,
false,
true
]
},
"hash": "f60053118df6a791d31fa258ee3737881f8f97ca41cbebd92eb22c967292d2ee"
}

View File

@ -341,6 +341,46 @@ impl Availability {
Ok(availabilities)
}
pub async fn read_by_user_and_daterange(
pool: &PgPool,
user_id: i32,
date_range: (&NaiveDate, &NaiveDate),
) -> Result<Vec<Availability>> {
let records = query!(
r##"
SELECT
availability.id,
availability.userId,
availability.startTimestamp,
availability.endTimestamp,
availability.comment
FROM availability
WHERE availability.userId = $1
AND availability.starttimestamp::date >= $2
AND availability.endtimestamp::date <= $3;
"##,
user_id,
date_range.0,
date_range.1
)
.fetch_all(pool)
.await?;
let availabilities = records
.iter()
.map(|r| Availability {
id: r.id,
user_id: r.userid,
user: None,
start: r.starttimestamp.naive_utc(),
end: r.endtimestamp.naive_utc(),
comment: r.comment.clone(),
})
.collect();
Ok(availabilities)
}
pub async fn find_adjacent_by_time_for_user(
pool: &PgPool,
start: &NaiveDateTime,

View File

@ -2,9 +2,11 @@ use chrono::{Days, NaiveDateTime};
use sqlx::PgPool;
use super::Availability;
use crate::{validation::{
start_date_time_lies_before_end_date_time, AsyncValidate, AsyncValidateError
}, END_OF_DAY, START_OF_DAY};
use crate::{
END_OF_DAY, START_OF_DAY,
models::Assignment,
validation::{AsyncValidate, AsyncValidateError, start_date_time_lies_before_end_date_time},
};
pub struct AvailabilityChangeset {
pub time: (NaiveDateTime, NaiveDateTime),
@ -14,7 +16,7 @@ pub struct AvailabilityChangeset {
pub struct AvailabilityContext<'a> {
pub pool: &'a PgPool,
pub user_id: i32,
pub availability_to_get_edited: Option<i32>,
pub availability: Option<i32>,
}
impl<'a> AsyncValidate<'a> for AvailabilityChangeset {
@ -28,48 +30,69 @@ impl<'a> AsyncValidate<'a> for AvailabilityChangeset {
Availability::read_by_user_and_date(context.pool, context.user_id, &self.time.0.date())
.await?;
if let Some(existing) = context.availability_to_get_edited {
start_date_time_lies_before_end_date_time(&self.time.0, &self.time.1)?;
if let Some(availability) = context.availability {
existing_availabilities = existing_availabilities
.into_iter()
.filter(|a| a.id != existing)
.filter(|a| a.id != availability)
.collect();
time_is_not_already_assigned(&self.time, availability, context.pool).await?;
}
time_is_not_already_made_available(&self.time, &existing_availabilities)?;
start_date_time_lies_before_end_date_time(&self.time.0, &self.time.1)?;
if !existing_availabilities.is_empty() {
time_is_not_already_made_available(&self.time, &existing_availabilities)?;
}
Ok(())
}
}
fn time_is_not_already_made_available(
value: &(NaiveDateTime, NaiveDateTime),
(start, end): &(NaiveDateTime, NaiveDateTime),
existing_availabilities: &Vec<Availability>,
) -> Result<(), AsyncValidateError> {
if existing_availabilities.is_empty() {
return Ok(());
}
let free_slots = find_free_date_time_slots(existing_availabilities);
if free_slots.is_empty() {
return Err(AsyncValidateError::new(
"cant create a availability as every time slot is already filled",
"Verfügbarkeit kann nicht erstellt werden, da bereits alle Zeiträume verfügbar gemacht wurden.",
));
}
let free_block_found_for_start = free_slots.iter().any(|s| s.0 <= value.0 && s.1 >= value.0);
let free_block_found_for_end = free_slots.iter().any(|s| s.0 <= value.1 && s.1 >= value.1);
let free_block_found_for_start = free_slots.iter().any(|s| s.0 <= *start && s.1 >= *start);
let free_block_found_for_end = free_slots.iter().any(|s| s.0 <= *end && s.1 >= *end);
let is_already_present_as_is = existing_availabilities
.iter()
.any(|a| a.start == *start && a.end == a.end);
if !free_block_found_for_start || !free_block_found_for_end {
if !free_block_found_for_start || !free_block_found_for_end || is_already_present_as_is {
return Err(AsyncValidateError::new(
"cant create availability as there exists already a availability with the desired time",
"Verfügbarkeit kann nicht erstellt werden, da eine vorhandene Verfügbarkeit überschnitten würde.",
));
}
Ok(())
}
async fn time_is_not_already_assigned(
(start, end): &(NaiveDateTime, NaiveDateTime),
availability: i32,
pool: &PgPool,
) -> Result<(), AsyncValidateError> {
let existing_assignments = Assignment::read_all_by_availability(pool, availability).await?;
for a in existing_assignments {
if a.start < *start || a.end > *end {
return Err(AsyncValidateError::new(
"Verfügbarkeitszeit kann nicht verkleinert werden, da bereits eine Planung für diese Zeit existiert.",
));
}
}
Ok(())
}
pub fn find_free_date_time_slots(
availabilities: &[Availability],
) -> Vec<(NaiveDateTime, NaiveDateTime)> {

View File

@ -95,6 +95,73 @@ impl Event {
Ok(events)
}
pub async fn read_all_by_daterange_and_area_including_location(
pool: &PgPool,
date_range: (&NaiveDate, &NaiveDate),
area_id: i32,
) -> Result<Vec<Event>> {
let records = query!(
r#"
SELECT
event.id AS eventId,
event.startTimestamp,
event.endTimestamp,
event.name,
event.locationId,
event.voluntaryWachhabender,
event.voluntaryFuehrungsassistent,
event.amountOfPosten,
event.clothing,
event.canceled,
event.note,
location.id,
location.name AS locationName,
location.areaId AS locationAreaId,
clothing.id AS clothingId,
clothing.name AS clothingName
FROM event
JOIN location ON event.locationId = location.id
JOIN clothing ON event.clothing = clothing.id
WHERE starttimestamp::date >= $1
AND starttimestamp::date <= $2
AND location.areaId = $3;
"#,
date_range.0,
date_range.1,
area_id
)
.fetch_all(pool)
.await?;
let events = records
.into_iter()
.map(|record| Event {
id: record.eventid,
start: record.starttimestamp.naive_utc(),
end: record.endtimestamp.naive_utc(),
name: record.name.to_string(),
location_id: record.locationid,
location: Some(Location {
id: record.locationid,
name: record.locationname.to_string(),
area_id: record.locationareaid,
area: None,
}),
voluntary_wachhabender: record.voluntarywachhabender,
voluntary_fuehrungsassistent: record.voluntaryfuehrungsassistent,
amount_of_posten: record.amountofposten,
clothing: Clothing {
id: record.clothingid,
name: record.clothingname,
},
canceled: record.canceled,
note: record.note,
})
.collect();
Ok(events)
}
pub async fn read_by_id_including_location(pool: &PgPool, id: i32) -> Result<Option<Event>> {
let record = query!(
r#"

View File

@ -5,8 +5,8 @@ mod validation_trait;
use chrono::NaiveDateTime;
pub use email::email_is_valid;
pub use error::AsyncValidateError;
pub use validation_trait::AsyncValidate;
use sqlx::PgPool;
pub use validation_trait::AsyncValidate;
pub struct DbContext<'a> {
pub pool: &'a PgPool,
@ -24,7 +24,7 @@ pub fn start_date_time_lies_before_end_date_time(
) -> Result<(), AsyncValidateError> {
if start >= end {
return Err(AsyncValidateError::new(
"endtime can't lie before starttime",
"Ende kann nicht vor dem Beginn liegen.",
));
}

View File

@ -0,0 +1,158 @@
use actix_web::{web, HttpResponse, Responder};
use askama::Template;
use chrono::{NaiveDate, Utc};
use serde::Deserialize;
use sqlx::PgPool;
use crate::{
filters,
utils::{
event_planning_template::generate_vehicles_assigned_and_available,
ApplicationError,
DateTimeFormat::{DayMonthYearHourMinute, HourMinute, WeekdayDayMonthYear},
TemplateResponse,
},
};
use brass_db::models::{
find_free_date_time_slots, Area, Assignment, Availability, Event, Function, Role, User, Vehicle,
};
#[derive(Deserialize)]
pub struct CalendarQuery {
date: Option<NaiveDate>,
area: Option<i32>,
}
#[derive(Template)]
#[template(path = "calendar.html")]
struct CalendarTemplate {
user: User,
user_can_create_availability: bool,
date: NaiveDate,
selected_area: Option<i32>,
areas: Vec<Area>,
events_and_assignments: Vec<(
Event,
Vec<String>,
Option<String>,
Option<String>,
Vec<Vehicle>,
)>,
availabilities: Vec<Availability>,
}
#[actix_web::get("/calendar")]
async fn get(
user: web::ReqData<User>,
pool: web::Data<PgPool>,
query: web::Query<CalendarQuery>,
) -> Result<impl Responder, ApplicationError> {
let date = match query.date {
Some(given_date) => given_date,
None => Utc::now().date_naive(),
};
let areas = Area::read_all(pool.get_ref()).await?;
let selected_area = match query.area {
Some(id) => {
if !areas.iter().any(|a| a.id == id) {
return Ok(HttpResponse::BadRequest().finish());
}
Some(id)
}
None => None,
};
let availabilities = Availability::read_by_date_and_area_including_user(
pool.get_ref(),
date,
query.area.unwrap_or(user.area_id),
)
.await?;
let availabilities_from_user =
Availability::read_by_user_and_date(pool.get_ref(), user.id, &date).await?;
//println!("{availabilities_from_user:#?}");
let user_can_create_availability = availabilities_from_user.is_empty()
|| !find_free_date_time_slots(&availabilities_from_user).is_empty();
//println!("{} || {} || {} = {user_can_create_availability}", availabilities_from_user.is_empty(),
// !only_one_availability_exists_and_is_whole_day(&availabilities_from_user),
// !find_free_time_slots(&availabilities_from_user).is_empty());
let mut events_and_assignments = Vec::new();
for e in Event::read_all_by_date_and_area_including_location(
pool.get_ref(),
date,
query.area.unwrap_or(user.area_id),
)
.await?
.into_iter()
{
let assignments = Assignment::read_all_by_event(pool.get_ref(), e.id).await?;
let (posten, rest): (Vec<Assignment>, Vec<Assignment>) = assignments
.into_iter()
.partition(|a| a.function == Function::Posten);
let (wachhabender, fuehrungsassistent): (Vec<Assignment>, Vec<Assignment>) = rest
.into_iter()
.partition(|a| a.function == Function::Wachhabender);
let (assigned_vehicle, _) = generate_vehicles_assigned_and_available(&pool, &e).await?;
events_and_assignments.push((
e,
posten
.into_iter()
.map(|p| {
availabilities
.iter()
.find(|a| a.id == p.availability_id)
.unwrap()
.user
.as_ref()
.unwrap()
.name
.clone()
})
.collect(),
fuehrungsassistent.first().map(|fa| {
availabilities
.iter()
.find(|a| a.id == fa.availability_id)
.unwrap()
.user
.as_ref()
.unwrap()
.name
.clone()
}),
wachhabender.first().map(|wh| {
availabilities
.iter()
.find(|a| a.id == wh.availability_id)
.unwrap()
.user
.as_ref()
.unwrap()
.name
.clone()
}),
assigned_vehicle,
));
}
let template = CalendarTemplate {
user: user.into_inner(),
user_can_create_availability,
date,
selected_area,
areas,
events_and_assignments,
availabilities,
};
Ok(template.to_response()?)
}

View File

@ -1,43 +1,23 @@
use actix_web::{web, HttpResponse, Responder};
use actix_web::{web, Responder};
use askama::Template;
use chrono::{NaiveDate, Utc};
use serde::Deserialize;
use chrono::{Months, Utc};
use sqlx::PgPool;
use crate::{
filters,
utils::{
event_planning_template::generate_vehicles_assigned_and_available,
ApplicationError,
DateTimeFormat::{DayMonthYearHourMinute, HourMinute, WeekdayDayMonthYear},
TemplateResponse,
},
};
use brass_db::models::{
find_free_date_time_slots, Area, Assignment, Availability, Event, Function, Role, User, Vehicle,
};
#[derive(Deserialize)]
pub struct CalendarQuery {
date: Option<NaiveDate>,
area: Option<i32>,
}
use brass_db::models::{Assignment, Availability, Event, Function, Role, User};
#[derive(Template)]
#[template(path = "index.html")]
struct CalendarTemplate {
#[template(path = "overview.html")]
struct OverviewTemplate {
user: User,
user_can_create_availability: bool,
date: NaiveDate,
selected_area: Option<i32>,
areas: Vec<Area>,
events_and_assignments: Vec<(
Event,
Vec<String>,
Option<String>,
Option<String>,
Vec<Vehicle>,
)>,
events: Vec<Event>,
availabilities: Vec<Availability>,
}
@ -45,112 +25,24 @@ struct CalendarTemplate {
async fn get(
user: web::ReqData<User>,
pool: web::Data<PgPool>,
query: web::Query<CalendarQuery>,
) -> Result<impl Responder, ApplicationError> {
let date = match query.date {
Some(given_date) => given_date,
None => Utc::now().date_naive(),
};
let today = Utc::now().date_naive();
let next_month = today.checked_add_months(Months::new(1)).unwrap();
let areas = Area::read_all(pool.get_ref()).await?;
let selected_area = match query.area {
Some(id) => {
if !areas.iter().any(|a| a.id == id) {
return Ok(HttpResponse::BadRequest().finish());
}
Some(id)
}
None => None,
};
let availabilities = Availability::read_by_date_and_area_including_user(
let events = Event::read_all_by_daterange_and_area_including_location(
pool.get_ref(),
date,
query.area.unwrap_or(user.area_id),
(&today, &next_month),
user.area_id,
)
.await?;
let availabilities_from_user =
Availability::read_by_user_and_date(pool.get_ref(), user.id, &date).await?;
//println!("{availabilities_from_user:#?}");
let availabilities =
Availability::read_by_user_and_daterange(pool.get_ref(), user.id, (&today, &next_month))
.await?;
let user_can_create_availability = availabilities_from_user.is_empty()
|| !find_free_date_time_slots(&availabilities_from_user).is_empty();
//println!("{} || {} || {} = {user_can_create_availability}", availabilities_from_user.is_empty(),
// !only_one_availability_exists_and_is_whole_day(&availabilities_from_user),
// !find_free_time_slots(&availabilities_from_user).is_empty());
let mut events_and_assignments = Vec::new();
for e in Event::read_all_by_date_and_area_including_location(
pool.get_ref(),
date,
query.area.unwrap_or(user.area_id),
)
.await?
.into_iter()
{
let assignments = Assignment::read_all_by_event(pool.get_ref(), e.id).await?;
let (posten, rest): (Vec<Assignment>, Vec<Assignment>) = assignments
.into_iter()
.partition(|a| a.function == Function::Posten);
let (wachhabender, fuehrungsassistent): (Vec<Assignment>, Vec<Assignment>) = rest
.into_iter()
.partition(|a| a.function == Function::Wachhabender);
let (assigned_vehicle, _) = generate_vehicles_assigned_and_available(&pool, &e).await?;
events_and_assignments.push((
e,
posten
.into_iter()
.map(|p| {
availabilities
.iter()
.find(|a| a.id == p.availability_id)
.unwrap()
.user
.as_ref()
.unwrap()
.name
.clone()
})
.collect(),
fuehrungsassistent.first().map(|fa| {
availabilities
.iter()
.find(|a| a.id == fa.availability_id)
.unwrap()
.user
.as_ref()
.unwrap()
.name
.clone()
}),
wachhabender.first().map(|wh| {
availabilities
.iter()
.find(|a| a.id == wh.availability_id)
.unwrap()
.user
.as_ref()
.unwrap()
.name
.clone()
}),
assigned_vehicle,
));
}
let template = CalendarTemplate {
let template = OverviewTemplate {
user: user.into_inner(),
user_can_create_availability,
date,
selected_area,
areas,
events_and_assignments,
events,
availabilities,
};

View File

@ -9,6 +9,7 @@ use crate::{
use brass_db::models::{Role, User};
pub mod delete;
pub mod get_calendar;
pub mod get_new;
pub mod get_overview;
pub mod get_update;

View File

@ -1,4 +1,5 @@
use actix_web::{http::header::LOCATION, web, HttpResponse, Responder};
use maud::html;
use sqlx::PgPool;
use crate::{
@ -22,7 +23,7 @@ pub async fn post(
let context = AvailabilityContext {
pool: pool.get_ref(),
user_id: user.id,
availability_to_get_edited: None,
availability: None,
};
let mut changeset = AvailabilityChangeset {
@ -31,7 +32,13 @@ pub async fn post(
};
if let Err(e) = changeset.validate_with_context(&context).await {
return Ok(HttpResponse::BadRequest().body(e.to_string()));
let error_message = html! {
svg class="icon is-small" {
use href="/static/feather-sprite.svg#alert-triangle" {}
}
" " (e)
};
return Ok(HttpResponse::UnprocessableEntity().body(error_message.into_string()));
};
if let Some(a) =

View File

@ -1,4 +1,5 @@
use actix_web::{http::header::LOCATION, web, HttpResponse, Responder};
use maud::html;
use sqlx::PgPool;
use crate::{
@ -31,7 +32,7 @@ pub async fn post(
let context = AvailabilityContext {
pool: pool.get_ref(),
user_id: user.id,
availability_to_get_edited: Some(availability.id),
availability: Some(availability.id),
};
let mut changeset = AvailabilityChangeset {
@ -40,7 +41,13 @@ pub async fn post(
};
if let Err(e) = changeset.validate_with_context(&context).await {
return Ok(HttpResponse::BadRequest().body(e.to_string()));
let error_message = html! {
svg class="icon is-small" {
use href="/static/feather-sprite.svg#alert-triangle" {}
}
" " (e)
};
return Ok(HttpResponse::UnprocessableEntity().body(error_message.into_string()));
};
if let Some(a) = Availability::find_adjacent_by_time_for_user(

View File

@ -56,10 +56,11 @@ pub fn init(cfg: &mut ServiceConfig) {
cfg.service(availability::delete::delete);
cfg.service(availability::get_new::get);
cfg.service(availability::get_overview::get);
cfg.service(availability::get_calendar::get);
cfg.service(availability::get_update::get);
cfg.service(availability::post_new::post);
cfg.service(availability::post_update::post);
cfg.service(availability::get_overview::get);
cfg.service(events::put_cancelation::put_cancel);
cfg.service(events::put_cancelation::put_uncancel);

View File

@ -4,7 +4,8 @@
<section class="section">
<div class="container">
{% let is_edit = id.is_some() %}
<form method="post" action="/availability/{% if is_edit %}edit/{{ id.unwrap() }}{% else %}new{% endif %}">
<form hx-post="/availability/{% if is_edit %}edit/{{ id.unwrap() }}{% else %}new{% endif %}" hx-target="body"
hx-target-422="#error">
<h1 class="title">{% if is_edit %}Bearbeite{% else %}Neue{% endif %} Vefügbarkeit für {{
date|fmt_date(WeekdayDayMonthYear) }}</h1>
@ -18,7 +19,7 @@
<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">
_="on change put the value of me into #st then put '' into #error">
{% if slot_suggestions.len() > 0 %}
<p class="help">noch mögliche Zeiträume:</p>
<div class="tags help">
@ -32,7 +33,7 @@
</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
_='on change put the value of me into #et then put "" into #error
then if (value of the previous <input/>) is greater than (value of me)
then set the value of #enddate to "{{ datetomorrow }}"
then put "{{ datetomorrow|fmt_date(WeekdayDayMonth) }}" into #ed
@ -53,13 +54,15 @@
<label class="radio">
<input type="radio" name="isovernight" {{ is_overnight|invert|ref|cond_show("checked")|safe }}
_='on click set the value of #enddate to "{{ date }}"
then put "{{ date|fmt_date(WeekdayDayMonth) }}" into #ed'>
then put "{{ date|fmt_date(WeekdayDayMonth) }}" into #ed
then put "" into #error'>
am selben Tag
</label>
<label class="radio ml-3">
<input type="radio" id="radionextday" name="isovernight" {{ is_overnight|cond_show("checked")|safe }}
_='on click set the value of #enddate to "{{ datetomorrow }}"
then put "{{ datetomorrow|fmt_date(WeekdayDayMonth) }}" into #ed'>
then put "{{ datetomorrow|fmt_date(WeekdayDayMonth) }}" into #ed
then put "" into #error'>
am Tag darauf
</label>
</div>
@ -90,6 +93,7 @@
<span id="et">{{ end_time|fmt_time(HourMinute) }}</span>
Uhr
</p>
<p id="error" class="help is-danger"></p>
</div>
</div>
</div>
@ -99,7 +103,7 @@
<div class="field-body">
<div class="field is-grouped">
<div class="control">
<button class="button is-success">
<button class="button is-success" _="on click put '' into #error">
<svg class="icon">
<use href="/static/feather-sprite.svg#check-circle" />
</svg>

View File

@ -18,6 +18,10 @@
<div class="navbar-menu" id="navMenu">
<div hx-boost="true" class="navbar-start">
<a href="/" class="navbar-item">
Übersicht
</a>
<a href="/calendar" class="navbar-item">
Kalender
</a>

View File

@ -0,0 +1,9 @@
{% extends "nav.html" %}
{% block content %}
<section id="progress" class="section">
<div class="container">
Übersicht
</div>
</section>
{% endblock %}

View File

@ -3,7 +3,8 @@
{% block content %}
<section class="section">
<div class="container">
<form hx-post="/users/{% if let Some(id) = id %}edit/{{ id }}{% else %}new{% endif %}" hx-target-422="find p">
<form hx-post="/users/{% if let Some(id) = id %}edit/{{ id }}{% else %}new{% endif %}" hx-target="body"
hx-target-422="find p">
<h1 class="title">
{% if let Some(name) = name %}Nutzer '{{ name }}' bearbeiten{% else %}Neuen Nutzer anlegen{% endif %}
</h1>