feat: WIP implement event planning

This commit is contained in:
Max Hohlfeld 2024-11-21 23:17:28 +01:00
parent 91046d0e1c
commit 95ca418875
25 changed files with 1771 additions and 364 deletions

View File

@ -34,3 +34,6 @@ thiserror = "1.0.63"
[build-dependencies]
built = "0.7.4"
static-files = "0.2.1"
[profile.dev.package.askama_derive]
opt-level = 3

View File

@ -20,10 +20,6 @@ fn main() -> std::io::Result<()> {
nm_path.join("feather-icons/dist/feather-sprite.svg"),
dist_path.join("feather-sprite.svg"),
)?;
//copy(
// nm_path.join("bulma/css/bulma.min.css"),
// dist_path.join("bulma.min.css"),
//)?;
copy(
nm_path.join("htmx.org/dist/htmx.min.js"),
dist_path.join("htmx.min.js"),
@ -33,7 +29,7 @@ fn main() -> std::io::Result<()> {
dist_path.join("response-targets.js"),
)?;
copy(
nm_path.join("sweetalert2/dist/sweetalert2.min.js"),
nm_path.join("sweetalert2-neutral/dist/sweetalert2.min.js"),
dist_path.join("sweetalert2.min.js"),
)?;
copy(

View File

@ -24,23 +24,23 @@ pub struct NewAssignmentTemplate {
#[actix_web::get("/assignments/new")]
pub async fn get(user: Identity, pool: web::Data<PgPool>, query: web::Query<EventQuery>) -> impl Responder {
let current_user = User::read_by_id(pool.get_ref(), user.id().unwrap().parse().unwrap())
.await
.unwrap();
if let Ok(event) = Event::read_by_id_including_location(pool.get_ref(), query.event).await {
if current_user.role == Role::Admin || current_user.role == Role::AreaManager && event.location.as_ref().unwrap().area_id == current_user.area_id {
let all = Availabillity::read_not_assigned_by_date_including_user(pool.get_ref(), event.date).await.unwrap();
let available_posten = Availabillity::read_not_assigned_by_date_including_user(pool.get_ref(), event.date).await.unwrap();
let available_wachhabende = available_posten.iter().filter(|a| a.user.as_ref().unwrap().function == Function::Wachhabender).map(|avl| avl.clone()).collect();
let template = NewAssignmentTemplate { user: current_user, event, available_wachhabende, available_posten, all };
return template.to_response();
}
return HttpResponse::Unauthorized().finish();
}
//let current_user = User::read_by_id(pool.get_ref(), user.id().unwrap().parse().unwrap())
// .await
// .unwrap();
//
//if let Ok(event) = Event::read_by_id_including_location(pool.get_ref(), query.event).await {
// if current_user.role == Role::Admin || current_user.role == Role::AreaManager && event.location.as_ref().unwrap().area_id == current_user.area_id {
// let all = Availabillity::read_not_assigned_by_date_including_user(pool.get_ref(), event.date).await.unwrap();
// let available_posten = Availabillity::read_not_assigned_by_date_including_user(pool.get_ref(), event.date).await.unwrap();
// let available_wachhabende = available_posten.iter().filter(|a| a.user.as_ref().unwrap().function == Function::Wachhabender).map(|avl| avl.clone()).collect();
//
// let template = NewAssignmentTemplate { user: current_user, event, available_wachhabende, available_posten, all };
//
// return template.to_response();
// }
//
// return HttpResponse::Unauthorized().finish();
//}
HttpResponse::BadRequest().body("Fehler beim Laden für Assignment")
}

View File

@ -13,47 +13,47 @@ pub struct NewAssignmentsForm {
#[actix_web::post("/assignments/new")]
pub async fn post(pool: web::Data<PgPool>, form: web::Form<Vec<(String, String)>>) -> impl Responder {
let event_id = form.iter().find(|x| x.0 == "event").unwrap().1.parse().unwrap();
let wachhabender = form.iter().find(|x| x.0 == "wachhabender");
let posten: Vec<i32> = form.iter().filter(|x| x.0 == "posten").map(|x| x.1.parse().unwrap()).collect();
let event = Event::read_by_id_including_location(&pool, event_id).await.unwrap(); // TODO: Check if location is needed
if event.voluntary_wachhabender && wachhabender.is_some() && posten.contains(&wachhabender.unwrap().1.parse().unwrap()) {
return HttpResponse::BadRequest().body("Wachhabender kann nicht zugleich Posten sein!");
}
let mut joined_ids = posten.clone();
if let Some((_,id)) = wachhabender {
joined_ids.push(id.parse().unwrap());
}
for availabillity_id in joined_ids {
let assignments = Assignment::read_by_availabillity(pool.get_ref(), availabillity_id).await.unwrap();
let mut can_be_used = true;
for assignment in assignments {
if event.start_time >= assignment.start_time && event.start_time <= assignment.end_time {
} else {
can_be_used = false;
break;
}
}
if !can_be_used {
return HttpResponse::BadRequest().body("availabillity time slot bereits genutzt!");
}
}
if let Some((_,id)) = wachhabender {
Assignment::create(pool.get_ref(), event.id, id.parse().unwrap(), Function::Wachhabender, event.start_time, event.end_time).await.unwrap();
}
for id in posten {
Assignment::create(pool.get_ref(), event.id, id, Function::Posten, event.start_time, event.end_time).await.unwrap();
}
//let event_id = form.iter().find(|x| x.0 == "event").unwrap().1.parse().unwrap();
//let wachhabender = form.iter().find(|x| x.0 == "wachhabender");
//let posten: Vec<i32> = form.iter().filter(|x| x.0 == "posten").map(|x| x.1.parse().unwrap()).collect();
//
//let event = Event::read_by_id_including_location(&pool, event_id).await.unwrap(); // TODO: Check if location is needed
//
//if event.voluntary_wachhabender && wachhabender.is_some() && posten.contains(&wachhabender.unwrap().1.parse().unwrap()) {
// return HttpResponse::BadRequest().body("Wachhabender kann nicht zugleich Posten sein!");
//}
//
//let mut joined_ids = posten.clone();
//if let Some((_,id)) = wachhabender {
// joined_ids.push(id.parse().unwrap());
//}
//
//for availabillity_id in joined_ids {
// let assignments = Assignment::read_by_availabillity(pool.get_ref(), availabillity_id).await.unwrap();
//
// let mut can_be_used = true;
//
//
// for assignment in assignments {
// if event.start_time >= assignment.start_time && event.start_time <= assignment.end_time {
// } else {
// can_be_used = false;
// break;
// }
// }
//
// if !can_be_used {
// return HttpResponse::BadRequest().body("availabillity time slot bereits genutzt!");
// }
//}
//
//if let Some((_,id)) = wachhabender {
// Assignment::create(pool.get_ref(), event.id, id.parse().unwrap(), Function::Wachhabender, event.start_time, event.end_time).await.unwrap();
//}
//
//for id in posten {
// Assignment::create(pool.get_ref(), event.id, id, Function::Posten, event.start_time, event.end_time).await.unwrap();
//}
//
HttpResponse::Ok().finish()
}

View File

@ -3,7 +3,7 @@ use sqlx::PgPool;
use crate::{
endpoints::IdPath,
models::{Availabillity, User},
models::{Availabillity, User}, utils::ApplicationError,
};
#[actix_web::delete("/availabillity/delete/{id}")]
@ -11,14 +11,16 @@ pub async fn delete(
user: web::ReqData<User>,
pool: web::Data<PgPool>,
path: web::Path<IdPath>,
) -> impl Responder {
if let Ok(availabillity_in_db) = Availabillity::read_by_id(pool.get_ref(), path.id).await {
if availabillity_in_db.user_id == user.id {
if let Ok(_) = Availabillity::delete(pool.get_ref(), availabillity_in_db.id).await {
return HttpResponse::Ok().finish();
}
}
) -> Result<impl Responder, ApplicationError> {
let Some(availabillity) = Availabillity::read_by_id(pool.get_ref(), path.id).await? else {
return Ok(HttpResponse::NotFound().finish());
};
if availabillity.user_id == user.id {
return Err(ApplicationError::Unauthorized);
}
return HttpResponse::BadRequest().finish();
Availabillity::delete(pool.get_ref(), availabillity.id).await?;
Ok(HttpResponse::Ok().finish())
}

View File

@ -49,7 +49,7 @@ async fn get(
None => None,
};
let events = Event::read_by_date_and_area_including_location(
let events = Event::read_all_by_date_and_area_including_location(
pool.get_ref(),
date,
query.area.unwrap_or(user.area_id),

View File

@ -6,6 +6,7 @@ use sqlx::PgPool;
use crate::{
endpoints::{availability::NewOrEditAvailabilityTemplate, IdPath},
models::{Availabillity, User},
utils::ApplicationError,
};
#[derive(Deserialize)]
@ -20,32 +21,34 @@ pub async fn get(
pool: web::Data<PgPool>,
path: web::Path<IdPath>,
query: web::Query<EditAvailabilityQuery>,
) -> impl Responder {
if let Ok(availabillity) = Availabillity::read_by_id(pool.get_ref(), path.id).await {
if availabillity.user_id == user.id {
let start_time = availabillity
.start_time
.and_then(|d| Some(d.format("%R").to_string()));
) -> Result<impl Responder, ApplicationError> {
let Some(availabillity) = Availabillity::read_by_id(pool.get_ref(), path.id).await? else {
return Ok(HttpResponse::NotFound().finish());
};
let end_time = availabillity
.end_time
.and_then(|d| Some(d.format("%R").to_string()));
let has_time = availabillity.start_time.is_some() && availabillity.end_time.is_some();
let template = NewOrEditAvailabilityTemplate {
user: user.into_inner(),
date: availabillity.date,
whole_day: query.whole_day.unwrap_or(!has_time),
id: Some(path.id),
start_time: start_time.as_deref(),
end_time: end_time.as_deref(),
comment: availabillity.comment.as_deref(),
};
return template.to_response();
}
if availabillity.user_id == user.id {
return Err(ApplicationError::Unauthorized);
}
HttpResponse::BadRequest().body("Availabillity with this id doesn't exist.")
let start_time = availabillity
.start_time
.and_then(|d| Some(d.format("%R").to_string()));
let end_time = availabillity
.end_time
.and_then(|d| Some(d.format("%R").to_string()));
let has_time = availabillity.start_time.is_some() && availabillity.end_time.is_some();
let template = NewOrEditAvailabilityTemplate {
user: user.into_inner(),
date: availabillity.date,
whole_day: query.whole_day.unwrap_or(!has_time),
id: Some(path.id),
start_time: start_time.as_deref(),
end_time: end_time.as_deref(),
comment: availabillity.comment.as_deref(),
};
Ok(template.to_response())
}

View File

@ -3,7 +3,10 @@ use chrono::{NaiveDate, NaiveTime};
use serde::Deserialize;
use sqlx::PgPool;
use crate::models::{Availabillity, User};
use crate::{
models::{Availabillity, User},
utils::{self, ApplicationError},
};
#[derive(Deserialize)]
pub struct AvailabillityForm {
@ -18,8 +21,8 @@ pub async fn post(
user: web::ReqData<User>,
pool: web::Data<PgPool>,
form: web::Form<AvailabillityForm>,
) -> impl Responder {
if let Ok(_) = Availabillity::create(
) -> Result<impl Responder, ApplicationError> {
Availabillity::create(
pool.get_ref(),
user.id,
form.date,
@ -27,12 +30,11 @@ pub async fn post(
form.till,
form.comment.clone(),
)
.await
{
HttpResponse::Found()
.insert_header((LOCATION, "/"))
.finish()
} else {
HttpResponse::BadRequest().body("Fehler beim erstellen")
}
.await?;
let url = utils::get_return_url_for_date(&form.date);
Ok(HttpResponse::Found()
.insert_header((LOCATION, url.clone()))
.insert_header(("HX-LOCATION", url))
.finish())
}

View File

@ -4,6 +4,7 @@ use sqlx::PgPool;
use crate::{
endpoints::{availability::post_new::AvailabillityForm, IdPath},
models::{Availabillity, User},
utils::{self, ApplicationError},
};
#[actix_web::post("/availabillity/edit/{id}")]
@ -12,42 +13,32 @@ pub async fn post(
pool: web::Data<PgPool>,
path: web::Path<IdPath>,
form: web::Form<AvailabillityForm>,
) -> impl Responder {
if let Ok(mut availabillity) = Availabillity::read_by_id(pool.get_ref(), path.id).await {
if availabillity.user_id == user.id {
let mut has_changed = false;
) -> Result<impl Responder, ApplicationError> {
let Some(availabillity) = Availabillity::read_by_id(pool.get_ref(), path.id).await? else {
return Ok(HttpResponse::NotFound().finish());
};
if availabillity.start_time != form.from {
availabillity.start_time = form.from;
has_changed = true;
}
if availabillity.end_time != form.till {
availabillity.end_time = form.till;
has_changed = true;
}
if availabillity.comment != form.comment {
availabillity.comment = form.comment.clone();
has_changed = true;
}
if has_changed {
if let Ok(_) = Availabillity::update(pool.get_ref(), path.id, &availabillity).await
{
return HttpResponse::Found()
.insert_header((LOCATION, "/"))
.finish();
}
}
if !has_changed {
return HttpResponse::Found()
.insert_header((LOCATION, "/"))
.finish();
}
}
if availabillity.user_id != user.id {
return Err(ApplicationError::Unauthorized);
}
HttpResponse::BadRequest().body("Fehler beim erstellen")
if availabillity.start_time != form.from
|| availabillity.end_time != form.till
|| availabillity.comment != form.comment
{
Availabillity::update(
pool.get_ref(),
availabillity.id,
form.from,
form.till,
form.comment.as_ref(),
)
.await?;
}
let url = utils::get_return_url_for_date(&form.date);
Ok(HttpResponse::Found()
.insert_header((LOCATION, url.clone()))
.insert_header(("HX-LOCATION", url))
.finish())
}

View File

@ -1,38 +1,44 @@
use actix_identity::Identity;
use actix_web::{web, HttpResponse, Responder};
use actix_web::{web, Responder};
use askama::Template;
use askama_actix::TemplateToResponse;
use chrono::NaiveDate;
use sqlx::PgPool;
use crate::{endpoints::NaiveDateQuery, models::{Role, User, Location}};
use crate::{
endpoints::NaiveDateQuery,
models::{Location, Role, User},
utils::ApplicationError,
};
#[derive(Template)]
#[template(path = "events/new.html")]
pub struct NewEventTemplate {
user: User,
date: NaiveDate,
locations: Vec<Location>
locations: Vec<Location>,
}
#[actix_web::get("/events/new")]
pub async fn get(user: Identity, pool: web::Data<PgPool>, query: web::Query<NaiveDateQuery>) -> impl Responder {
let current_user = User::read_by_id(pool.get_ref(), user.id().unwrap().parse().unwrap())
.await
.unwrap();
if current_user.role != Role::Admin && current_user.role != Role::AreaManager {
return HttpResponse::Unauthorized().finish();
pub async fn get(
user: web::ReqData<User>,
pool: web::Data<PgPool>,
query: web::Query<NaiveDateQuery>,
) -> Result<impl Responder, ApplicationError> {
if user.role != Role::Admin && user.role != Role::AreaManager {
return Err(ApplicationError::Unauthorized);
}
let locations;
if current_user.role == Role::Admin {
locations = Location::read_all_including_area(pool.get_ref()).await.unwrap();
let locations = if user.role == Role::Admin {
Location::read_all_including_area(pool.get_ref()).await?
} else {
locations = Location::read_by_area(pool.get_ref(), current_user.area_id).await.unwrap();
}
Location::read_by_area(pool.get_ref(), user.area_id).await?
};
let template = NewEventTemplate { user: current_user, date: query.date, locations };
let template = NewEventTemplate {
user: user.into_inner(),
date: query.date,
locations,
};
return template.to_response();
Ok(template.to_response())
}

View File

@ -0,0 +1,101 @@
use actix_web::{web, HttpResponse, Responder};
use askama::Template;
use askama_actix::TemplateToResponse;
use chrono::NaiveDate;
use sqlx::PgPool;
use crate::{
endpoints::{IdPath, NaiveDateQuery},
models::{Assignment, Availabillity, Event, Function, Location, Role, User},
utils::ApplicationError,
};
#[derive(Template)]
#[template(path = "events/plan.html")]
pub struct PlanEventTemplate {
user: User,
event: Event,
availabillities: Vec<(Availabillity, AssignmentState)>,
}
enum AssignmentState {
// availabillity is not assigned at all or at least not timely conflicting
Unassigned,
// availabillity is assigned for another event that is timely conflicting
Conflicting,
// availabillity is assigned to this event as Posten
AssignedPosten,
// availabillity is assigned to this event as Führungsassistent
AssignedFührungsassistent,
// availabillity is assigned to this event as Wachhabender
AssignedWachahabender,
}
#[actix_web::get("/events/{id}/plan")]
pub async fn get(
user: web::ReqData<User>,
pool: web::Data<PgPool>,
path: web::Path<IdPath>,
) -> Result<impl Responder, ApplicationError> {
if user.role != Role::Admin && user.role != Role::AreaManager {
return Err(ApplicationError::Unauthorized);
}
let Some(event) = Event::read_by_id_including_location(pool.get_ref(), path.id).await? else {
return Ok(HttpResponse::NotFound().finish());
};
if user.role != Role::Admin && user.area_id != event.location.as_ref().unwrap().area_id {
return Err(ApplicationError::Unauthorized);
}
let availabillities_in_db = Availabillity::read_by_date_and_area_including_user(
pool.get_ref(),
event.date,
event.location.as_ref().unwrap().area_id,
)
.await?;
let mut availabillities = Vec::new();
for availabillity in availabillities_in_db {
let assignments =
Assignment::read_all_by_availabillity(pool.get_ref(), availabillity.id).await?;
if let Some(assignment) = assignments
.iter()
.find(|assignment| assignment.event_id == event.id)
{
let state = match assignment.function {
Function::Posten => AssignmentState::AssignedPosten,
Function::Fuehrungsassistent => AssignmentState::AssignedFührungsassistent,
Function::Wachhabender => AssignmentState::AssignedWachahabender,
};
availabillities.push((availabillity, state));
continue;
}
let has_start_time_during_event =
|a: &Assignment| a.start_time >= event.start_time && a.start_time <= event.end_time;
let has_end_time_during_event =
|a: &Assignment| a.end_time >= event.start_time && a.end_time <= event.end_time;
if assignments
.iter()
.any(|a| has_start_time_during_event(a) || has_end_time_during_event(a))
{
availabillities.push((availabillity, AssignmentState::Conflicting));
continue;
}
availabillities.push((availabillity, AssignmentState::Unassigned));
}
let template = PlanEventTemplate {
user: user.into_inner(),
event,
availabillities,
};
Ok(template.to_response())
}

View File

@ -1,2 +1,3 @@
pub mod get_new;
pub mod post_new;
pub mod get_plan;

View File

@ -1,10 +1,9 @@
use actix_identity::Identity;
use actix_web::{http::header::LOCATION, web, HttpResponse, Responder};
use chrono::{NaiveDate, NaiveTime};
use serde::Deserialize;
use sqlx::PgPool;
use crate::models::{Event, Role, User};
use crate::{models::{Event, Role, User}, utils::{self, ApplicationError}};
#[derive(Deserialize)]
pub struct NewEventForm {
@ -16,25 +15,20 @@ pub struct NewEventForm {
voluntarywachhabender: Option<bool>,
amount: i16,
clothing: String,
note: Option<String>
}
#[actix_web::post("/events/new")]
pub async fn post(
user: Identity,
user: web::ReqData<User>,
pool: web::Data<PgPool>,
form: web::Form<NewEventForm>,
) -> impl Responder {
let current_user = User::read_by_id(pool.get_ref(), user.id().unwrap().parse().unwrap())
.await
.unwrap();
if current_user.role != Role::Admin && current_user.role != Role::AreaManager {
return HttpResponse::Unauthorized().finish();
) -> Result<impl Responder, ApplicationError> {
if user.role != Role::Admin && user.role != Role::AreaManager {
return Err(ApplicationError::Unauthorized);
}
let return_location = format!("/?data={}", form.date);
match Event::create(
Event::create(
pool.get_ref(),
&form.date,
&form.from,
@ -44,9 +38,14 @@ pub async fn post(
form.voluntarywachhabender.is_some() && form.voluntarywachhabender.unwrap(),
form.amount,
&form.clothing,
form.note.as_ref()
)
.await {
Ok(_) => HttpResponse::Found().insert_header((LOCATION, return_location)).finish(),
Err(_) => HttpResponse::BadRequest().body("Fehler beim Erstellen")
}
.await?;
let url = utils::get_return_url_for_date(&form.date);
println!("redirecto to {url}");
Ok(HttpResponse::Found()
.insert_header((LOCATION, url.clone()))
.insert_header(("HX-LOCATION", url))
.finish())
}

View File

@ -57,6 +57,7 @@ pub fn init(cfg: &mut ServiceConfig) {
cfg.service(events::get_new::get);
cfg.service(events::post_new::post);
cfg.service(events::get_plan::get);
cfg.service(assignment::get_new::get);
cfg.service(assignment::post_new::post);

View File

@ -1,7 +1,7 @@
use chrono::NaiveTime;
use sqlx::{query, PgPool};
use super::Function;
use super::{Function, Result};
pub struct Assignment {
pub event_id: i32,
@ -19,7 +19,7 @@ impl Assignment {
function: Function,
start_time: NaiveTime,
end_time: NaiveTime,
) -> anyhow::Result<()> {
) -> Result<()> {
query!(
r##"
INSERT INTO assignment (eventId, availabillityId, function, startTime, endTime)
@ -37,13 +37,13 @@ impl Assignment {
Ok(())
}
pub async fn read_by_availabillity(
pub async fn read_all_by_availabillity(
pool: &PgPool,
availabillity_id: i32,
) -> anyhow::Result<Vec<Assignment>> {
) -> Result<Vec<Assignment>> {
let records = query!(
r##"
SELECT
SELECT
assignment.eventId,
assignment.availabillityId,
assignment.function AS "function: Function",

View File

@ -1,7 +1,7 @@
use chrono::{NaiveDate, NaiveTime};
use sqlx::{query, PgPool};
use super::{function::Function, role::Role, user::User, Area};
use super::{Area, Function, Result, Role, User};
#[derive(Clone)]
pub struct Availabillity {
@ -22,22 +22,29 @@ impl Availabillity {
start_time: Option<NaiveTime>,
end_time: Option<NaiveTime>,
comment: Option<String>,
) -> anyhow::Result<i32> {
let result = match (start_time, end_time, comment) {
(Some(start_time), Some(end_time), Some(comment)) => query!("INSERT INTO availabillity (userId, date, startTime, endTime, comment) VALUES ($1, $2, $3, $4, $5) RETURNING id;", user_id, date, start_time, end_time, comment).fetch_one(pool).await?.id,
(Some(start_time), Some(end_time), None) => query!("INSERT INTO availabillity (userId, date, startTime, endTime) VALUES ($1, $2, $3, $4) RETURNING id;", user_id, date, start_time, end_time).fetch_one(pool).await?.id,
(None, None, Some(comment)) => query!("INSERT INTO availabillity (userId, date, comment) VALUES ($1, $2, $3) RETURNING id;", user_id, date, comment).fetch_one(pool).await?.id,
(_, _, _) => query!("INSERT INTO availabillity (userId, date) VALUES ($1, $2) RETURNING id;", user_id, date).fetch_one(pool).await?.id
};
) -> Result<()> {
query!(
r#"
INSERT INTO availabillity (userId, date, startTime, endTime, comment)
VALUES ($1, $2, $3, $4, $5);
"#,
user_id,
date,
start_time,
end_time,
comment
)
.execute(pool)
.await?;
Ok(result)
Ok(())
}
pub async fn read_by_date_and_area_including_user(
pool: &PgPool,
date: NaiveDate,
area_id: i32
) -> anyhow::Result<Vec<Availabillity>> {
area_id: i32,
) -> Result<Vec<Availabillity>> {
let records = query!(
r##"
SELECT
@ -100,7 +107,7 @@ impl Availabillity {
pub async fn read_not_assigned_by_date_including_user(
pool: &PgPool,
date: NaiveDate,
) -> anyhow::Result<Vec<Availabillity>> {
) -> Result<Vec<Availabillity>> {
let records = query!(
r##"
SELECT
@ -159,20 +166,22 @@ impl Availabillity {
Ok(availabillities)
}
pub async fn read_by_id(pool: &PgPool, id: i32) -> anyhow::Result<Availabillity> {
pub async fn read_by_id(pool: &PgPool, id: i32) -> Result<Option<Availabillity>> {
let record = query!("SELECT * FROM availabillity WHERE id = $1", id)
.fetch_one(pool)
.fetch_optional(pool)
.await?;
let availabillity = Availabillity {
id: record.id,
user_id: record.userid,
user: None,
date: record.date,
start_time: record.starttime,
end_time: record.endtime,
comment: record.comment.clone(),
};
let availabillity = record.and_then(|record| {
Some(Availabillity {
id: record.id,
user_id: record.userid,
user: None,
date: record.date,
start_time: record.starttime,
end_time: record.endtime,
comment: record.comment.clone(),
})
});
Ok(availabillity)
}
@ -181,7 +190,7 @@ impl Availabillity {
pool: &PgPool,
date_range: (NaiveDate, NaiveDate),
area_id: i32,
) -> anyhow::Result<Vec<Availabillity>> {
) -> Result<Vec<Availabillity>> {
let records = query!(
r##"
SELECT
@ -251,13 +260,15 @@ impl Availabillity {
pub async fn update(
pool: &PgPool,
id: i32,
updated_availabillity: &Availabillity,
) -> anyhow::Result<()> {
start_time: Option<NaiveTime>,
end_time: Option<NaiveTime>,
comment: Option<&String>,
) -> Result<()> {
query!(
"UPDATE availabillity SET startTime = $1, endTime = $2, comment = $3 WHERE id = $4",
updated_availabillity.start_time,
updated_availabillity.end_time,
updated_availabillity.comment,
start_time,
end_time,
comment,
id
)
.execute(pool)
@ -266,7 +277,7 @@ impl Availabillity {
Ok(())
}
pub async fn delete(pool: &PgPool, id: i32) -> anyhow::Result<()> {
pub async fn delete(pool: &PgPool, id: i32) -> Result<()> {
query!("DELETE FROM availabillity WHERE id = $1", id)
.execute(pool)
.await?;

View File

@ -1,8 +1,9 @@
use chrono::{NaiveDate, NaiveTime};
use sqlx::{query, PgPool};
use super::Location;
use super::{Location, Result};
#[derive(Debug)]
pub struct Event {
pub id: i32,
pub date: NaiveDate,
@ -15,6 +16,7 @@ pub struct Event {
pub amount_of_posten: i16,
pub clothing: String,
pub canceled: bool,
pub note: Option<String>,
}
impl Event {
@ -28,17 +30,22 @@ impl Event {
voluntary_wachhabender: bool,
amount_of_posten: i16,
clothing: &String,
) -> anyhow::Result<i32> {
let result = query!("INSERT INTO event (date, startTime, endTime, name, locationId, voluntaryWachhabender, amountOfPosten, clothing) VALUES ($1, $2, $3, $4, $5, $6, $7, $8) RETURNING id;", date, start_time, end_time, name, location_id, voluntary_wachhabender, amount_of_posten, clothing).fetch_one(pool).await?;
note: Option<&String>,
) -> Result<()> {
query!(r#"
INSERT INTO event (date, startTime, endTime, name, locationId, voluntaryWachhabender, amountOfPosten, clothing, note)
VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9);
"#,
date, start_time, end_time, name, location_id, voluntary_wachhabender, amount_of_posten, clothing, note).execute(pool).await?;
Ok(result.id)
Ok(())
}
pub async fn read_by_date_and_area_including_location(
pub async fn read_all_by_date_and_area_including_location(
pool: &PgPool,
date: NaiveDate,
area_id: i32
) -> anyhow::Result<Vec<Event>> {
area_id: i32,
) -> Result<Vec<Event>> {
let records = query!(
r#"
SELECT
@ -52,6 +59,7 @@ impl Event {
event.amountOfPosten,
event.clothing,
event.canceled,
event.note,
location.id,
location.name AS locationName,
location.areaId AS locationAreaId
@ -67,7 +75,7 @@ impl Event {
.await?;
let events = records
.iter()
.into_iter()
.map(|record| Event {
id: record.eventid,
date: record.date,
@ -85,13 +93,14 @@ impl Event {
amount_of_posten: record.amountofposten,
clothing: record.clothing.to_string(),
canceled: record.canceled,
note: record.note,
})
.collect();
Ok(events)
}
pub async fn read_by_id_including_location(pool: &PgPool, id: i32) -> anyhow::Result<Event> {
pub async fn read_by_id_including_location(pool: &PgPool, id: i32) -> Result<Option<Event>> {
let record = query!(
r#"
SELECT
@ -105,6 +114,7 @@ impl Event {
event.amountOfPosten,
event.clothing,
event.canceled,
event.note,
location.id,
location.name AS locationName,
location.areaId AS locationAreaId
@ -114,27 +124,30 @@ impl Event {
"#,
id
)
.fetch_one(pool)
.fetch_optional(pool)
.await?;
let event = Event {
id: record.eventid,
date: record.date,
start_time: record.starttime,
end_time: record.endtime,
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,
amount_of_posten: record.amountofposten,
clothing: record.clothing.to_string(),
canceled: record.canceled,
};
let event = record.and_then(|record| {
Some(Event {
id: record.eventid,
date: record.date,
start_time: record.starttime,
end_time: record.endtime,
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,
amount_of_posten: record.amountofposten,
clothing: record.clothing.to_string(),
canceled: record.canceled,
note: record.note,
})
});
Ok(event)
}

View File

@ -2,7 +2,10 @@ use sqlx::{query, PgPool};
use super::Area;
pub struct Location {
use super::Result;
#[derive(Debug)]
pub struct Location {
pub id: i32,
pub name: String,
pub area_id: i32,
@ -10,19 +13,19 @@ pub struct Location {
}
impl Location {
pub async fn create(pool: &PgPool, name: &str, area_id: i32) -> anyhow::Result<i32> {
let result = query!(
"INSERT INTO location (name, areaId) VALUES ($1, $2) RETURNING id;",
pub async fn create(pool: &PgPool, name: &str, area_id: i32) -> Result<()> {
query!(
"INSERT INTO location (name, areaId) VALUES ($1, $2);",
name,
area_id
)
.fetch_one(pool)
.await?;
Ok(result.id)
Ok(())
}
pub async fn read_by_area(pool: &PgPool, area_id: i32) -> anyhow::Result<Vec<Location>> {
pub async fn read_by_area(pool: &PgPool, area_id: i32) -> Result<Vec<Location>> {
let records = query!("SELECT * FROM location WHERE areaId = $1;", area_id)
.fetch_all(pool)
.await?;
@ -40,7 +43,7 @@ impl Location {
Ok(locations)
}
pub async fn read_all(pool: &PgPool) -> anyhow::Result<Vec<Location>> {
pub async fn read_all(pool: &PgPool) -> Result<Vec<Location>> {
let records = query!("SELECT * FROM location").fetch_all(pool).await?;
let locations = records
@ -56,7 +59,7 @@ impl Location {
Ok(locations)
}
pub async fn read_all_including_area(pool: &PgPool) -> anyhow::Result<Vec<Location>> {
pub async fn read_all_including_area(pool: &PgPool) -> Result<Vec<Location>> {
let records = query!("SELECT location.id AS locationId, location.name, location.areaId, area.id, area.name AS areaName FROM location JOIN area ON location.areaId = area.id;")
.fetch_all(pool)
.await?;
@ -77,7 +80,7 @@ impl Location {
Ok(locations)
}
pub async fn read_by_id(pool: &PgPool, id: i32) -> super::Result<Option<Location>> {
pub async fn read_by_id(pool: &PgPool, id: i32) -> Result<Option<Location>> {
let record = query!("SELECT * FROM location WHERE id = $1;", id)
.fetch_optional(pool)
.await?;
@ -94,7 +97,7 @@ impl Location {
Ok(location)
}
pub async fn update(pool: &PgPool, id: i32, name: &str, area_id: i32) -> super::Result<()> {
pub async fn update(pool: &PgPool, id: i32, name: &str, area_id: i32) -> Result<()> {
query!(
"UPDATE location SET name = $1, areaid = $2 WHERE id = $3;",
name,
@ -107,7 +110,7 @@ impl Location {
Ok(())
}
pub async fn delete(pool: &PgPool, id: i32) -> super::Result<()> {
pub async fn delete(pool: &PgPool, id: i32) -> Result<()> {
query!("DELETE FROM location WHERE id = $1;", id)
.execute(pool)
.await?;

View File

@ -5,3 +5,13 @@ pub mod token_generation;
mod application_error;
pub use application_error::ApplicationError;
use chrono::{NaiveDate, Utc};
pub fn get_return_url_for_date(date: &NaiveDate) -> String {
let today = Utc::now().date_naive();
if date == &today {
return String::from("/");
}
format!("/?date={}", date)
}

1394
static/package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@ -1,10 +1,10 @@
{
"devDependencies": {
"bulma": "^1.0.1",
"bulma": "^1.0.2",
"feather-icons": "^4.29.2",
"htmx.org": "^1.9.12",
"sass": "^1.77.8",
"sweetalert2": "^11.12.4"
"sweetalert2-neutral": "^11.14.1-neutral-fix6"
},
"scripts": {
"build-bulma": "sass --load-path=node_modules --no-source-map style.scss dist/style.css"

View File

@ -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,
@ -34,6 +34,8 @@ $crimson: #00d1b2; //#B80F0A;
@forward "bulma/sass/layout/section";
@forward "bulma/sass/layout/hero";
@forward "bulma/sass/grid";
@forward "bulma/sass/helpers/spacing";
@forward "bulma/sass/helpers/flexbox";
@ -41,7 +43,7 @@ $crimson: #00d1b2; //#B80F0A;
@forward "bulma/sass/themes";
// TODO: bulma theme for sweetalert looks and feels outdated
@use "sweetalert2/src/sweetalert2.scss" with ($swal2-confirm-button-background-color: $crimson);
@use "sweetalert2-neutral/src/sweetalert2.scss" with ($swal2-confirm-button-background-color: $crimson);
[class*=" icon"],
[class^=icon] {

View File

@ -96,6 +96,19 @@
</div>
</div>
<div class="field is-horizontal">
<div class="field-label">
<label class="label">Anmerkung</label>
</div>
<div class="field-body">
<div class="field">
<div class="control">
<input class="input" name="note" placeholder="" />
</div>
</div>
</div>
</div>
<div class="field is-horizontal">
<div class="field-label"></div>
<div class="field-body">

View File

@ -0,0 +1,64 @@
{% extends "nav.html" %}
{% block content %}
<section class="section">
<div class="container">
<h1 class="title">Eventplanung</h1>
<div class="box">
<h5 class="title is-5">Allgemeines</h5>
<div class="fixed-grid has-1-cols-mobile">
<div class="grid content">
<div class="cell is-col-span-2">
<p><b>Name:</b> {{ event.name }}</p>
</div>
<div class="cell">
<p><b>Datum:</b> {{ event.date.format("%d.%m.%Y") }}</p>
</div>
<div class="cell">
<p><b>Uhrzeit:</b> {{ event.start_time.format("%R") }} Uhr - {{ event.end_time.format("%R") }} Uhr</p>
</div>
<div class="cell is-col-span-2">
<p><b>Veranstaltungsort:</b> {{ event.location.as_ref().unwrap().name }}</p>
</div>
<div class="cell">
<p><b>Wachhabender:</b> {% if event.voluntary_wachhabender %}FF{% else %}BF{% endif %}</p>
</div>
<div class="cell">
<p><b>Führungsassistent:</b> {% if event.voluntary_wachhabender %}FF{% else %}BF{% endif %}</p>
</div>
<div class="cell is-col-span-2">
<p><b>Anzahl der Posten:</b> {{ event.amount_of_posten }}</p>
</div>
<div class="cell is-col-span-2">
<p><b>Anzugsordnung:</b> {{ event.clothing }}</p>
</div>
<div class="cell is-col-span-2">
<p><b>Anmerkungen:</b> {{ event.note.as_ref().unwrap_or(String::new()|as_ref) }}</p>
</div>
</div>
</div>
</div>
<div class="box">
<h5 class="title is-5">Einteilung Personal</h5>
<!--TODO: next: Use availabillities-->
</div>
<div class="box">
<h5 class="title is-5">Einteilung Fahrzeuge</h5>
</div>
</div>
</section>
{% endblock %}

View File

@ -56,7 +56,7 @@
Events am {{ date.format("%d.%m.%Y") }}
</h3>
</div>
{% if (user.role == Role::Admin || user.role == Role::AreaManager) && (selected_area.is_none() ||
{% if user.role == Role::Admin || user.role == Role::AreaManager && (selected_area.is_none() ||
selected_area.unwrap() == user.area_id) %}
<div class="level-right">
<a class="button is-link is-light" hx-boost="true" href="/events/new?date={{ date }}">
@ -81,7 +81,7 @@
<h5 class="title is-5 level-left">{{ event.name }}</h5>
<span class="level-right">
{% if user.role == Role::AreaManager || user.role == Role::Admin %}
<a href="/assignments/new?event={{ event.id }}" hx-boost="true"
<a href="/events/{{ event.id }}/plan" hx-boost="true"
class="button is-primary level-item">Planen</a>
<a href="" class="button is-primary-light level-item">bearbeiten</a>
<a href="" class="button is-warning level-item">als abgesagt markieren</a>