refactor: WIP datetime event and assignments

This commit is contained in:
Max Hohlfeld 2025-05-04 21:35:26 +02:00
parent 93e6a79a38
commit 8b61bb37a8
23 changed files with 187 additions and 207 deletions

View File

@ -88,21 +88,25 @@ mod tests {
let start = NaiveTime::from_hms_opt(10, 0, 0).unwrap(); let start = NaiveTime::from_hms_opt(10, 0, 0).unwrap();
let end = NaiveTime::from_hms_opt(15, 30, 0).unwrap(); let end = NaiveTime::from_hms_opt(15, 30, 0).unwrap();
let new_event = EventChangeset::create_for_test(date.clone(), start.clone(), end.clone()); let new_event = EventChangeset::create_for_test(date.and_time(start), date.and_time(end));
Event::create(pool, new_event).await?; Event::create(pool, new_event).await?;
let new_availability = AvailabilityChangeset { let new_availability = AvailabilityChangeset {
time: (date.and_hms_opt(12, 0, 0).unwrap(), date.and_hms_opt(15, 0, 0).unwrap()), time: (
date.and_hms_opt(12, 0, 0).unwrap(),
date.and_hms_opt(15, 0, 0).unwrap(),
),
comment: None, comment: None,
}; };
Availability::create(pool, 1, new_availability).await?; Availability::create(pool, 1, new_availability).await?;
let new_assignment = AssignmentChangeset { let new_assignment = AssignmentChangeset {
function: Function::Posten, function: Function::Posten,
time: (start, end), time: (date.and_time(start), date.and_time(end)),
}; };
Assignment::create(pool, 1, 1, new_assignment).await?; Assignment::create(pool, 1, 1, new_assignment).await?;
// TODO: check this test after large refactoring
Ok(()) Ok(())
} }

View File

@ -61,7 +61,7 @@ pub async fn post(
let changeset = AssignmentChangeset { let changeset = AssignmentChangeset {
function, function,
time: (event.start_time, event.end_time), time: (event.start, event.end),
}; };
let assignments_for_event = Assignment::read_all_by_event(pool.get_ref(), event.id).await?; let assignments_for_event = Assignment::read_all_by_event(pool.get_ref(), event.id).await?;

View File

@ -1,5 +1,4 @@
use actix_web::{http::header::LOCATION, web, HttpResponse, Responder}; use actix_web::{http::header::LOCATION, web, HttpResponse, Responder};
use chrono::NaiveTime;
use garde::Validate; use garde::Validate;
use sqlx::PgPool; use sqlx::PgPool;
@ -10,7 +9,6 @@ use crate::{
}, },
models::{Availability, AvailabilityChangeset, AvailabilityContext, User}, models::{Availability, AvailabilityChangeset, AvailabilityContext, User},
utils::{self, ApplicationError}, utils::{self, ApplicationError},
END_OF_DAY,
}; };
#[actix_web::post("/availabillity/edit/{id}")] #[actix_web::post("/availabillity/edit/{id}")]

View File

@ -33,7 +33,7 @@ pub async fn delete(
Event::delete(pool.get_ref(), event.id).await?; Event::delete(pool.get_ref(), event.id).await?;
let url = utils::get_return_url_for_date(&event.date); let url = utils::get_return_url_for_date(&event.start.date());
Ok(HttpResponse::Ok() Ok(HttpResponse::Ok()
.insert_header(("HX-LOCATION", url)) .insert_header(("HX-LOCATION", url))
.finish()) .finish())

View File

@ -39,7 +39,7 @@ pub async fn get(
let template = NewEventTemplate { let template = NewEventTemplate {
user: user.into_inner(), user: user.into_inner(),
date: event.date, date: event.start.date(),
locations, locations,
event: Some(event), event: Some(event),
amount_of_planned_posten: assignments amount_of_planned_posten: assignments
@ -85,11 +85,12 @@ async fn produces_template(context: &DbTestContext) {
.await .await
.unwrap(); .unwrap();
let date = NaiveDate::parse_from_str("2025-01-01", "%F").unwrap();
let changeset = crate::models::EventChangeset { let changeset = crate::models::EventChangeset {
date: NaiveDate::parse_from_str("2025-01-01", "%F").unwrap(),
time: ( time: (
NaiveTime::parse_from_str("08:00", "%R").unwrap(), date.and_time(NaiveTime::parse_from_str("08:00", "%R").unwrap()),
NaiveTime::parse_from_str("10:00", "%R").unwrap(), date.and_time(NaiveTime::parse_from_str("10:00", "%R").unwrap()),
), ),
name: "Vorstellung".to_string(), name: "Vorstellung".to_string(),
location_id: 1, location_id: 1,

View File

@ -1,3 +1,4 @@
use chrono::Days;
use crate::filters; use crate::filters;
use chrono::NaiveDate; use chrono::NaiveDate;
use askama::Template; use askama::Template;

View File

@ -1,5 +1,5 @@
use actix_web::{http::header::LOCATION, web, HttpResponse, Responder}; use actix_web::{http::header::LOCATION, web, HttpResponse, Responder};
use chrono::{NaiveDate, NaiveTime}; use chrono::{Days, NaiveDateTime};
use garde::Validate; use garde::Validate;
use serde::Deserialize; use serde::Deserialize;
use sqlx::PgPool; use sqlx::PgPool;
@ -7,18 +7,18 @@ use sqlx::PgPool;
use crate::{ use crate::{
endpoints::IdPath, endpoints::IdPath,
models::{ models::{
Assignment, AssignmentChangeset, Availability, Event, EventChangeset, Assignment, AssignmentChangeset, Availability, Event, EventChangeset, EventContext,
EventContext, Function, Location, Role, User, Function, Location, Role, User,
}, },
utils::{self, ApplicationError}, utils::{self, ApplicationError},
END_OF_DAY, START_OF_DAY,
}; };
#[derive(Deserialize)] #[derive(Deserialize)]
pub struct EditEventForm { pub struct EditEventForm {
name: String, name: String,
date: NaiveDate, from: NaiveDateTime,
from: NaiveTime, till: NaiveDateTime,
till: NaiveTime,
location: i32, location: i32,
voluntarywachhabender: Option<bool>, voluntarywachhabender: Option<bool>,
voluntaryfuehrungsassistent: Option<bool>, voluntaryfuehrungsassistent: Option<bool>,
@ -58,7 +58,6 @@ pub async fn post(
} }
let changeset = EventChangeset { let changeset = EventChangeset {
date: form.date,
amount_of_posten: form.amount, amount_of_posten: form.amount,
clothing: form.clothing.clone(), clothing: form.clothing.clone(),
location_id: form.location, location_id: form.location,
@ -74,10 +73,10 @@ pub async fn post(
let assignments_for_event = Assignment::read_all_by_event(pool.get_ref(), event.id).await?; let assignments_for_event = Assignment::read_all_by_event(pool.get_ref(), event.id).await?;
let mut common_time = ( let start = event.start.date();
NaiveTime::parse_from_str("00:00", "%R").unwrap(), let end = event.start.date().checked_add_days(Days::new(1)).unwrap();
NaiveTime::parse_from_str("23:59", "%R").unwrap(), let mut common_time = (start.and_time(START_OF_DAY), end.and_time(END_OF_DAY));
);
for assignment in &assignments_for_event { for assignment in &assignments_for_event {
let availability = Availability::read_by_id(pool.get_ref(), assignment.availabillity_id) let availability = Availability::read_by_id(pool.get_ref(), assignment.availabillity_id)
.await? .await?
@ -87,39 +86,37 @@ pub async fn post(
.await?; .await?;
if all_assignments.len() == 1 { if all_assignments.len() == 1 {
// TODO: refactor if availability.start > common_time.0 {
if availability.start.time() > common_time.0 { common_time.0 = availability.start;
common_time.0 = availability.start.time(); }
}
if availability.end.time() < common_time.1 { if availability.end < common_time.1 {
common_time.1 = availability.end.time(); common_time.1 = availability.end;
} }
} else { } else {
// TODO: refactor let mut slots = vec![(availability.start, availability.end)];
let mut slots = vec![(availability.start.time(), availability.end.time())];
for a in all_assignments for a in all_assignments
.iter() .iter()
.filter(|x| x.event_id != assignment.event_id) .filter(|x| x.event_id != assignment.event_id)
{ {
let (fit, rest) = slots let (fit, rest) = slots
.into_iter() .into_iter()
.partition(|s| s.0 >= a.start_time && s.1 <= a.end_time); .partition(|s| s.0 >= a.start && s.1 <= a.end);
slots = rest; slots = rest;
let fit = fit.first().unwrap(); let fit = fit.first().unwrap();
if fit.0 != a.start_time { if fit.0 != a.start {
slots.push((fit.0, a.start_time)); slots.push((fit.0, a.start));
} }
if fit.1 != a.end_time { if fit.1 != a.end {
slots.push((a.end_time, fit.1)); slots.push((a.end, fit.1));
} }
} }
let slot = slots let slot = slots
.into_iter() .into_iter()
.find(|s| s.0 >= assignment.start_time && s.1 <= assignment.end_time) .find(|s| s.0 >= assignment.start && s.1 <= assignment.end)
.unwrap(); .unwrap();
if slot.0 > common_time.0 { if slot.0 > common_time.0 {
@ -133,7 +130,7 @@ pub async fn post(
} }
let context = Some(EventContext { let context = Some(EventContext {
date_in_db: event.date, date_in_db: event.start.date(),
common_min_max_available_time: common_time, common_min_max_available_time: common_time,
// safe as max amount of posten can be only 100 // safe as max amount of posten can be only 100
amount_of_assigned_posten: assignments_for_event amount_of_assigned_posten: assignments_for_event
@ -152,7 +149,7 @@ pub async fn post(
return Ok(HttpResponse::BadRequest().body(e.to_string())); return Ok(HttpResponse::BadRequest().body(e.to_string()));
}; };
if event.start_time != changeset.time.0 || event.end_time != changeset.time.1 { if event.start != changeset.time.0 || event.end != changeset.time.1 {
for a in assignments_for_event { for a in assignments_for_event {
let c = AssignmentChangeset { let c = AssignmentChangeset {
function: a.function, function: a.function,
@ -164,7 +161,7 @@ pub async fn post(
Event::update(pool.get_ref(), event.id, changeset).await?; Event::update(pool.get_ref(), event.id, changeset).await?;
let url = utils::get_return_url_for_date(&form.date); let url = utils::get_return_url_for_date(&form.from.date());
//println!("redirecto to {url}"); //println!("redirecto to {url}");
Ok(HttpResponse::Found() Ok(HttpResponse::Found()
.insert_header((LOCATION, url.clone())) .insert_header((LOCATION, url.clone()))

View File

@ -1,5 +1,5 @@
use actix_web::{http::header::LOCATION, web, HttpResponse, Responder}; use actix_web::{http::header::LOCATION, web, HttpResponse, Responder};
use chrono::{NaiveDate, NaiveTime}; use chrono::NaiveDateTime;
use garde::Validate; use garde::Validate;
use serde::Deserialize; use serde::Deserialize;
use sqlx::PgPool; use sqlx::PgPool;
@ -12,9 +12,8 @@ use crate::{
#[derive(Deserialize)] #[derive(Deserialize)]
pub struct NewEventForm { pub struct NewEventForm {
name: String, name: String,
date: NaiveDate, from: NaiveDateTime,
from: NaiveTime, till: NaiveDateTime,
till: NaiveTime,
location: i32, location: i32,
voluntarywachhabender: Option<bool>, voluntarywachhabender: Option<bool>,
voluntaryfuehrungsassistent: Option<bool>, voluntaryfuehrungsassistent: Option<bool>,
@ -42,7 +41,6 @@ pub async fn post(
} }
let changeset = EventChangeset { let changeset = EventChangeset {
date: form.date,
amount_of_posten: form.amount, amount_of_posten: form.amount,
clothing: form.clothing.clone(), clothing: form.clothing.clone(),
location_id: form.location, location_id: form.location,
@ -62,7 +60,7 @@ pub async fn post(
Event::create(pool.get_ref(), changeset).await?; Event::create(pool.get_ref(), changeset).await?;
let url = utils::get_return_url_for_date(&form.date); let url = utils::get_return_url_for_date(&form.from.date());
//println!("redirecto to {url}"); //println!("redirecto to {url}");
Ok(HttpResponse::Found() Ok(HttpResponse::Found()
.insert_header((LOCATION, url.clone())) .insert_header((LOCATION, url.clone()))

View File

@ -47,7 +47,7 @@ async fn handle_set_event_cancelation_to(
Event::update_cancelation(pool.get_ref(), event.id, cancelation_state).await?; Event::update_cancelation(pool.get_ref(), event.id, cancelation_state).await?;
} }
let url = utils::get_return_url_for_date(&event.date); let url = utils::get_return_url_for_date(&event.start.date());
Ok(HttpResponse::Ok() Ok(HttpResponse::Ok()
.insert_header(("HX-LOCATION", url)) .insert_header(("HX-LOCATION", url))
.finish()) .finish())

View File

@ -45,13 +45,16 @@ pub async fn post(
return Ok(HttpResponse::NotFound().finish()); return Ok(HttpResponse::NotFound().finish());
}; };
let existing_assignments_for_vehicle = let existing_assignments_for_vehicle = VehicleAssignement::read_all_by_vehicle_and_date(
VehicleAssignement::read_all_by_vehicle_and_date(pool.get_ref(), vehicle.id, event.date) pool.get_ref(),
.await?; vehicle.id,
event.start.date(),
)
.await?;
let has_start_time_during_event = let has_start_time_during_event =
|a: &VehicleAssignement| a.start_time >= event.start_time && a.start_time <= event.end_time; |a: &VehicleAssignement| a.start >= event.start && a.start <= event.end;
let has_end_time_during_event = let has_end_time_during_event =
|a: &VehicleAssignement| a.end_time >= event.start_time && a.end_time <= event.end_time; |a: &VehicleAssignement| a.end >= event.start && a.end <= event.end;
let availability_already_assigned = existing_assignments_for_vehicle let availability_already_assigned = existing_assignments_for_vehicle
.iter() .iter()
@ -62,14 +65,8 @@ pub async fn post(
.body("Vehicle already assigned to a timely conflicting event.")); .body("Vehicle already assigned to a timely conflicting event."));
} }
VehicleAssignement::create( VehicleAssignement::create(pool.get_ref(), event.id, vehicle.id, event.start, event.end)
pool.get_ref(), .await?;
event.id,
vehicle.id,
event.start_time,
event.end_time,
)
.await?;
let (vehicles_assigned, vehicles_available) = let (vehicles_assigned, vehicles_available) =
generate_vehicles_assigned_and_available(pool.get_ref(), &event).await?; generate_vehicles_assigned_and_available(pool.get_ref(), &event).await?;

View File

@ -1,3 +1,4 @@
use chrono::{NaiveDate, NaiveDateTime};
use maud::html; use maud::html;
use crate::models::UserFunction; use crate::models::UserFunction;
@ -57,3 +58,19 @@ pub fn show_tree(f: &UserFunction) -> askama::Result<String> {
Ok(html.into_string()) Ok(html.into_string())
} }
pub fn dt_f(v: &NaiveDateTime) -> askama::Result<String> {
Ok(v.format("%F").to_string())
}
pub fn dt_d(v: &NaiveDateTime) -> askama::Result<String> {
Ok(v.format("%d.%m.%Y").to_string())
}
pub fn date_d(v: &NaiveDate) -> askama::Result<String> {
Ok(v.format("%d.%m.%Y").to_string())
}
pub fn dt_t(v: &NaiveDateTime) -> askama::Result<String> {
Ok(v.format("%R").to_string())
}

View File

@ -1,4 +1,4 @@
use chrono::{Local, NaiveDateTime, NaiveTime}; use chrono::NaiveDateTime;
use sqlx::{query, PgPool}; use sqlx::{query, PgPool};
use super::{assignment_changeset::AssignmentChangeset, Function, Result}; use super::{assignment_changeset::AssignmentChangeset, Function, Result};
@ -7,8 +7,8 @@ pub struct Assignment {
pub event_id: i32, pub event_id: i32,
pub availabillity_id: i32, pub availabillity_id: i32,
pub function: Function, pub function: Function,
pub start_time: NaiveTime, pub start: NaiveDateTime,
pub end_time: NaiveTime, pub end: NaiveDateTime,
} }
impl Assignment { impl Assignment {
@ -18,10 +18,6 @@ impl Assignment {
availabillity_id: i32, availabillity_id: i32,
changeset: AssignmentChangeset, changeset: AssignmentChangeset,
) -> Result<()> { ) -> Result<()> {
// TODO: refactor
let date1 = Local::now();
let date2 = Local::now();
query!( query!(
r##" r##"
INSERT INTO assignment (eventId, availabillityId, function, startTimestamp, endTimestamp) INSERT INTO assignment (eventId, availabillityId, function, startTimestamp, endTimestamp)
@ -30,8 +26,8 @@ impl Assignment {
event_id, event_id,
availabillity_id, availabillity_id,
changeset.function as Function, changeset.function as Function,
date1, changeset.time.0.and_utc(),
date2 changeset.time.1.and_utc()
) )
.execute(pool) .execute(pool)
.await?; .await?;
@ -65,9 +61,8 @@ impl Assignment {
event_id: r.eventid, event_id: r.eventid,
availabillity_id: r.availabillityid, availabillity_id: r.availabillityid,
function: r.function, function: r.function,
// TODO: refactor start: r.starttimestamp.naive_utc(),
start_time: r.starttimestamp.time(), end: r.endtimestamp.naive_utc(),
end_time: r.endtimestamp.time(),
}) })
.collect(); .collect();
@ -97,9 +92,8 @@ impl Assignment {
event_id: r.eventid, event_id: r.eventid,
availabillity_id: r.availabillityid, availabillity_id: r.availabillityid,
function: r.function, function: r.function,
// TODO: refactor start: r.starttimestamp.naive_utc(),
start_time: r.starttimestamp.time(), end: r.endtimestamp.naive_utc(),
end_time: r.endtimestamp.time(),
}) })
.collect(); .collect();
@ -134,9 +128,8 @@ impl Assignment {
event_id: r.eventid, event_id: r.eventid,
availabillity_id: r.availabillityid, availabillity_id: r.availabillityid,
function: r.function, function: r.function,
// TODO: refactor start: r.starttimestamp.naive_utc(),
start_time: r.starttimestamp.time(), end: r.endtimestamp.naive_utc(),
end_time: r.endtimestamp.time(),
}); });
Ok(assignemnet) Ok(assignemnet)
@ -148,11 +141,7 @@ impl Assignment {
availabillity_id: i32, availabillity_id: i32,
changeset: AssignmentChangeset, changeset: AssignmentChangeset,
) -> Result<()> { ) -> Result<()> {
// TODO: refactor query!("UPDATE assignment SET function = $1, startTimestamp = $2, endTimestamp = $3 WHERE eventId = $4 AND availabillityId = $5;", changeset.function as Function, changeset.time.0.and_utc(), changeset.time.1.and_utc(), event_id, availabillity_id).execute(pool).await?;
let date1 = Local::now();
let date2 = Local::now();
query!("UPDATE assignment SET function = $1, startTimestamp = $2, endTimestamp = $3 WHERE eventId = $4 AND availabillityId = $5;", changeset.function as Function, date1, date2, event_id, availabillity_id).execute(pool).await?;
Ok(()) Ok(())
} }

View File

@ -1,8 +1,8 @@
use chrono::NaiveTime; use chrono::NaiveDateTime;
use garde::Validate; use garde::Validate;
use super::{ use super::{
start_time_lies_before_end_time, Assignment, Availability, Event, Function, UserFunction, start_date_time_lies_before_end_date_time, Assignment, Availability, Event, Function, UserFunction,
}; };
#[derive(Validate)] #[derive(Validate)]
@ -17,10 +17,10 @@ pub struct AssignmentChangeset {
pub function: Function, pub function: Function,
#[garde( #[garde(
custom(available_time_fits), custom(available_time_fits),
custom(start_time_lies_before_end_time), custom(start_date_time_lies_before_end_date_time),
custom(availabillity_not_already_assigned) custom(availabillity_not_already_assigned)
)] )]
pub time: (NaiveTime, NaiveTime), pub time: (NaiveDateTime, NaiveDateTime),
} }
pub struct AssignmentContext { pub struct AssignmentContext {
@ -32,16 +32,14 @@ pub struct AssignmentContext {
} }
fn available_time_fits( fn available_time_fits(
value: &(NaiveTime, NaiveTime), value: &(NaiveDateTime, NaiveDateTime),
context: &AssignmentContext, context: &AssignmentContext,
) -> garde::Result { ) -> garde::Result {
// if let AvailabilityTime::Temporarily(start, end) = context.availabillity.time { if value.0 < context.availabillity.start || value.1 > context.availabillity.end {
// if value.0 < start || value.1 > end { return Err(garde::Error::new(
// return Err(garde::Error::new( "time not made available can't be assigned",
// "time not made available can't be assigned", ));
// )); }
// }
// }
Ok(()) Ok(())
} }
@ -89,7 +87,7 @@ fn event_has_free_slot_for_function(
} }
fn availabillity_not_already_assigned( fn availabillity_not_already_assigned(
value: &(NaiveTime, NaiveTime), value: &(NaiveDateTime, NaiveDateTime),
context: &AssignmentContext, context: &AssignmentContext,
) -> garde::Result { ) -> garde::Result {
let list: Vec<&Assignment> = context let list: Vec<&Assignment> = context
@ -101,9 +99,8 @@ fn availabillity_not_already_assigned(
.collect(); .collect();
let has_start_time_during_assignment = let has_start_time_during_assignment =
|a: &Assignment| a.start_time >= value.0 && a.start_time <= value.1; |a: &Assignment| a.start >= value.0 && a.start <= value.1;
let has_end_time_during_assignment = let has_end_time_during_assignment = |a: &Assignment| a.end >= value.0 && a.end <= value.1;
|a: &Assignment| a.end_time >= value.0 && a.end_time <= value.1;
if list if list
.iter() .iter()

View File

@ -3,7 +3,7 @@ use garde::Validate;
use crate::{END_OF_DAY, START_OF_DAY}; use crate::{END_OF_DAY, START_OF_DAY};
use super::{start_date_time_lies_before_end_time, Availability}; use super::{start_date_time_lies_before_end_date_time, Availability};
#[derive(Validate)] #[derive(Validate)]
#[garde(allow_unvalidated)] #[garde(allow_unvalidated)]
@ -11,7 +11,7 @@ use super::{start_date_time_lies_before_end_time, Availability};
pub struct AvailabilityChangeset { pub struct AvailabilityChangeset {
#[garde( #[garde(
custom(time_is_not_already_made_available), custom(time_is_not_already_made_available),
custom(start_date_time_lies_before_end_time) custom(start_date_time_lies_before_end_date_time)
)] )]
pub time: (NaiveDateTime, NaiveDateTime), pub time: (NaiveDateTime, NaiveDateTime),
pub comment: Option<String>, pub comment: Option<String>,

View File

@ -1,4 +1,4 @@
use chrono::{Local, NaiveDate, NaiveDateTime, NaiveTime}; use chrono::{NaiveDate, NaiveDateTime};
use sqlx::{query, PgPool}; use sqlx::{query, PgPool};
use super::{Area, AvailabilityChangeset, Result, Role, User, UserFunction}; use super::{Area, AvailabilityChangeset, Result, Role, User, UserFunction};

View File

@ -1,4 +1,4 @@
use chrono::{Local, NaiveDate, NaiveTime}; use chrono::{NaiveDate, NaiveDateTime};
use sqlx::{query, PgPool}; use sqlx::{query, PgPool};
use super::{event_changeset::EventChangeset, Location, Result}; use super::{event_changeset::EventChangeset, Location, Result};
@ -6,9 +6,8 @@ use super::{event_changeset::EventChangeset, Location, Result};
#[derive(Clone, Debug)] #[derive(Clone, Debug)]
pub struct Event { pub struct Event {
pub id: i32, pub id: i32,
pub date: NaiveDate, pub start: NaiveDateTime,
pub start_time: NaiveTime, pub end: NaiveDateTime,
pub end_time: NaiveTime,
pub name: String, pub name: String,
pub location_id: i32, pub location_id: i32,
pub location: Option<Location>, pub location: Option<Location>,
@ -22,15 +21,11 @@ pub struct Event {
impl Event { impl Event {
pub async fn create(pool: &PgPool, changeset: EventChangeset) -> Result<()> { pub async fn create(pool: &PgPool, changeset: EventChangeset) -> Result<()> {
// TODO: refactor
let date1 = Local::now();
let date2 = Local::now();
query!(r#" query!(r#"
INSERT INTO event (startTimestamp, endTimestamp, name, locationId, voluntaryWachhabender, voluntaryFuehrungsassistent, amountOfPosten, clothing, note) INSERT INTO event (startTimestamp, endTimestamp, name, locationId, voluntaryWachhabender, voluntaryFuehrungsassistent, amountOfPosten, clothing, note)
VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9); VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9);
"#, "#,
date1, date2, changeset.name, changeset.location_id, changeset.voluntary_wachhabender, changeset.voluntary_fuehrungsassistent, changeset.amount_of_posten, changeset.clothing, changeset.note).execute(pool).await?; changeset.time.0.and_utc(), changeset.time.1.and_utc(), changeset.name, changeset.location_id, changeset.voluntary_wachhabender, changeset.voluntary_fuehrungsassistent, changeset.amount_of_posten, changeset.clothing, changeset.note).execute(pool).await?;
Ok(()) Ok(())
} }
@ -73,9 +68,8 @@ impl Event {
.into_iter() .into_iter()
.map(|record| Event { .map(|record| Event {
id: record.eventid, id: record.eventid,
date: record.starttimestamp.date_naive(), start: record.starttimestamp.naive_utc(),
start_time: record.starttimestamp.time(), end: record.endtimestamp.naive_utc(),
end_time: record.endtimestamp.time(),
name: record.name.to_string(), name: record.name.to_string(),
location_id: record.locationid, location_id: record.locationid,
location: Some(Location { location: Some(Location {
@ -125,9 +119,8 @@ impl Event {
let event = record.map(|record| Event { let event = record.map(|record| Event {
id: record.eventid, id: record.eventid,
date: record.starttimestamp.date_naive(), start: record.starttimestamp.naive_utc(),
start_time: record.starttimestamp.time(), end: record.endtimestamp.naive_utc(),
end_time: record.endtimestamp.time(),
name: record.name.to_string(), name: record.name.to_string(),
location_id: record.locationid, location_id: record.locationid,
location: Some(Location { location: Some(Location {
@ -148,16 +141,18 @@ impl Event {
} }
pub async fn update(pool: &PgPool, id: i32, changeset: EventChangeset) -> Result<()> { pub async fn update(pool: &PgPool, id: i32, changeset: EventChangeset) -> Result<()> {
// TODO: refactor
let date1 = Local::now();
let date2 = Local::now();
query!(r#" query!(r#"
UPDATE event SET startTimestamp = $1, endTimestamp = $2, name = $3, locationId = $4, voluntaryWachhabender = $5, voluntaryFuehrungsassistent = $6, amountOfPosten = $7, clothing = $8, note = $9 WHERE id = $10; UPDATE event SET startTimestamp = $1, endTimestamp = $2, name = $3, locationId = $4, voluntaryWachhabender = $5, voluntaryFuehrungsassistent = $6, amountOfPosten = $7, clothing = $8, note = $9 WHERE id = $10;
"#, "#,
date1, changeset.time.0.and_utc(),
date2 changeset.time.1.and_utc(),
,changeset.name, changeset.location_id, changeset.voluntary_wachhabender, changeset.voluntary_fuehrungsassistent, changeset.amount_of_posten, changeset.clothing, changeset.note, id) changeset.name,
changeset.location_id,
changeset.voluntary_wachhabender,
changeset.voluntary_fuehrungsassistent,
changeset.amount_of_posten,
changeset.clothing,
changeset.note, id)
.execute(pool) .execute(pool)
.await?; .await?;

View File

@ -1,22 +1,21 @@
use chrono::NaiveDate; use chrono::NaiveDate;
use chrono::NaiveTime; use chrono::NaiveDateTime;
#[cfg(test)] #[cfg(test)]
use fake::{Faker, Fake}; use fake::{Fake, Faker};
use garde::Validate; use garde::Validate;
use super::start_time_lies_before_end_time; use super::start_date_time_lies_before_end_date_time;
#[derive(Validate)] #[derive(Validate)]
#[garde(allow_unvalidated)] #[garde(allow_unvalidated)]
#[garde(context(Option<EventContext> as ctx))] #[garde(context(Option<EventContext> as ctx))]
pub struct EventChangeset { pub struct EventChangeset {
#[garde(custom(date_unchanged_if_edit))]
pub date: NaiveDate,
#[garde( #[garde(
custom(start_time_lies_before_end_time), custom(start_date_time_lies_before_end_date_time),
custom(time_can_be_extended_if_edit) custom(time_can_be_extended_if_edit),
custom(date_unchanged_if_edit)
)] )]
pub time: (NaiveTime, NaiveTime), pub time: (NaiveDateTime, NaiveDateTime),
pub name: String, pub name: String,
/// check before: must exist and user can create event for this location /// check before: must exist and user can create event for this location
pub location_id: i32, pub location_id: i32,
@ -32,9 +31,8 @@ pub struct EventChangeset {
#[cfg(test)] #[cfg(test)]
impl EventChangeset { impl EventChangeset {
pub fn create_for_test(date: NaiveDate, start: NaiveTime, end: NaiveTime) -> EventChangeset { pub fn create_for_test(start: NaiveDateTime, end: NaiveDateTime) -> EventChangeset {
let changeset = EventChangeset { let changeset = EventChangeset {
date,
time: (start, end), time: (start, end),
name: Faker.fake(), name: Faker.fake(),
location_id: 1, location_id: 1,
@ -51,14 +49,14 @@ impl EventChangeset {
pub struct EventContext { pub struct EventContext {
pub date_in_db: NaiveDate, pub date_in_db: NaiveDate,
pub common_min_max_available_time: (NaiveTime, NaiveTime), pub common_min_max_available_time: (NaiveDateTime, NaiveDateTime),
pub wachhabender_assigned: bool, pub wachhabender_assigned: bool,
pub fuehrungsassistent_assigned: bool, pub fuehrungsassistent_assigned: bool,
pub amount_of_assigned_posten: i16, pub amount_of_assigned_posten: i16,
} }
fn date_unchanged_if_edit(value: &NaiveDate, context: &Option<EventContext>) -> garde::Result { fn date_unchanged_if_edit(value: &(NaiveDateTime, NaiveDateTime), context: &Option<EventContext>) -> garde::Result {
if context.as_ref().is_some_and(|c| c.date_in_db != *value) { if context.as_ref().is_some_and(|c| c.date_in_db != value.0.date() ) {
return Err(garde::Error::new("event date can't be changed")); return Err(garde::Error::new("event date can't be changed"));
} }
@ -66,7 +64,7 @@ fn date_unchanged_if_edit(value: &NaiveDate, context: &Option<EventContext>) ->
} }
fn time_can_be_extended_if_edit( fn time_can_be_extended_if_edit(
value: &(NaiveTime, NaiveTime), value: &(NaiveDateTime, NaiveDateTime),
context: &Option<EventContext>, context: &Option<EventContext>,
) -> garde::Result { ) -> garde::Result {
if let Some(context) = context { if let Some(context) = context {

View File

@ -25,7 +25,7 @@ pub use availability_changeset::{
}; };
pub use availabillity::Availability; pub use availabillity::Availability;
pub use availabillity_assignment_state::AvailabillityAssignmentState; pub use availabillity_assignment_state::AvailabillityAssignmentState;
use chrono::{NaiveDateTime, NaiveTime}; use chrono::NaiveDateTime;
pub use event::Event; pub use event::Event;
pub use event_changeset::{EventChangeset, EventContext}; pub use event_changeset::{EventChangeset, EventContext};
pub use function::Function; pub use function::Function;
@ -41,18 +41,7 @@ pub use vehicle_assignement::VehicleAssignement;
type Result<T> = std::result::Result<T, sqlx::Error>; type Result<T> = std::result::Result<T, sqlx::Error>;
fn start_time_lies_before_end_time<T>( fn start_date_time_lies_before_end_date_time<T>(
value: &(NaiveTime, NaiveTime),
_context: &T,
) -> garde::Result {
if value.0 >= value.1 {
return Err(garde::Error::new("endtime can't lie before starttime"));
}
Ok(())
}
fn start_date_time_lies_before_end_time<T>(
value: &(NaiveDateTime, NaiveDateTime), value: &(NaiveDateTime, NaiveDateTime),
_context: &T, _context: &T,
) -> garde::Result { ) -> garde::Result {

View File

@ -1,4 +1,4 @@
use chrono::{Local, NaiveDate, NaiveTime}; use chrono::{NaiveDate, NaiveDateTime};
use sqlx::{query, PgPool}; use sqlx::{query, PgPool};
use super::Result; use super::Result;
@ -6,8 +6,8 @@ use super::Result;
pub struct VehicleAssignement { pub struct VehicleAssignement {
pub event_id: i32, pub event_id: i32,
pub vehicle_id: i32, pub vehicle_id: i32,
pub start_time: NaiveTime, pub start: NaiveDateTime,
pub end_time: NaiveTime, pub end: NaiveDateTime,
} }
impl VehicleAssignement { impl VehicleAssignement {
@ -15,19 +15,15 @@ impl VehicleAssignement {
pool: &PgPool, pool: &PgPool,
event_id: i32, event_id: i32,
vehicle_id: i32, vehicle_id: i32,
start_time: NaiveTime, start: NaiveDateTime,
end_time: NaiveTime, end: NaiveDateTime,
) -> Result<()> { ) -> Result<()> {
// TODO: refactor
let date1 = Local::now();
let date2 = Local::now();
query!( query!(
"INSERT INTO vehicleassignement (eventId, vehicleId, startTimestamp, endTimestamp) VALUES ($1, $2, $3, $4);", "INSERT INTO vehicleassignement (eventId, vehicleId, startTimestamp, endTimestamp) VALUES ($1, $2, $3, $4);",
event_id, event_id,
vehicle_id, vehicle_id,
date1, start.and_utc(),
date2 end.and_utc()
) )
.execute(pool) .execute(pool)
.await?; .await?;
@ -43,12 +39,11 @@ impl VehicleAssignement {
let record = query!("SELECT * FROM vehicleAssignement WHERE vehicleAssignement.eventId = $1 AND vehicleAssignement.vehicleId = $2;", event_id, vehicle_id).fetch_optional(pool) let record = query!("SELECT * FROM vehicleAssignement WHERE vehicleAssignement.eventId = $1 AND vehicleAssignement.vehicleId = $2;", event_id, vehicle_id).fetch_optional(pool)
.await?; .await?;
// TODO: refactor
let vehicle_assignment = record.map(|r| VehicleAssignement { let vehicle_assignment = record.map(|r| VehicleAssignement {
event_id: r.eventid, event_id: r.eventid,
vehicle_id: r.vehicleid, vehicle_id: r.vehicleid,
start_time: r.starttimestamp.time(), start: r.starttimestamp.naive_utc(),
end_time: r.endtimestamp.time(), end: r.endtimestamp.naive_utc(),
}); });
Ok(vehicle_assignment) Ok(vehicle_assignment)
@ -65,14 +60,13 @@ impl VehicleAssignement {
.fetch_all(pool) .fetch_all(pool)
.await?; .await?;
// TODO: refactor
let vehicle_assignments = records let vehicle_assignments = records
.iter() .iter()
.map(|r| VehicleAssignement { .map(|r| VehicleAssignement {
event_id: r.eventid, event_id: r.eventid,
vehicle_id: r.vehicleid, vehicle_id: r.vehicleid,
start_time: r.starttimestamp.time(), start: r.starttimestamp.naive_utc(),
end_time: r.endtimestamp.time(), end: r.endtimestamp.naive_utc(),
}) })
.collect(); .collect();
@ -82,7 +76,7 @@ impl VehicleAssignement {
pub async fn read_all_by_vehicle_and_date( pub async fn read_all_by_vehicle_and_date(
pool: &PgPool, pool: &PgPool,
vehicle_id: i32, vehicle_id: i32,
date: NaiveDate date: NaiveDate,
) -> Result<Vec<VehicleAssignement>> { ) -> Result<Vec<VehicleAssignement>> {
let records = query!( let records = query!(
r#" r#"
@ -109,8 +103,8 @@ impl VehicleAssignement {
.map(|r| VehicleAssignement { .map(|r| VehicleAssignement {
event_id: r.eventid, event_id: r.eventid,
vehicle_id: r.vehicleid, vehicle_id: r.vehicleid,
start_time: r.starttimestamp.time(), start: r.starttimestamp.naive_utc(),
end_time: r.endtimestamp.time(), end: r.endtimestamp.naive_utc(),
}) })
.collect(); .collect();

View File

@ -13,8 +13,8 @@ pub async fn generate_availabillity_assignment_list(
) -> Result<Vec<(Availability, AvailabillityAssignmentState)>, ApplicationError> { ) -> Result<Vec<(Availability, AvailabillityAssignmentState)>, ApplicationError> {
let availabillities_in_db = Availability::read_by_date_time_area_including_user( let availabillities_in_db = Availability::read_by_date_time_area_including_user(
pool, pool,
event.date, event.start.date(),
(event.start_time, event.end_time), (event.start, event.end),
event.location.as_ref().unwrap().area_id, event.location.as_ref().unwrap().area_id,
) )
.await?; .await?;
@ -46,9 +46,8 @@ pub async fn generate_availabillity_assignment_list(
} }
let has_start_time_during_event = let has_start_time_during_event =
|a: &Assignment| a.start_time >= event.start_time && a.start_time <= event.end_time; |a: &Assignment| a.start >= event.start && a.start <= event.end;
let has_end_time_during_event = let has_end_time_during_event = |a: &Assignment| a.end >= event.start && a.end <= event.end;
|a: &Assignment| a.end_time >= event.start_time && a.end_time <= event.end_time;
if assignments if assignments
.iter() .iter()

View File

@ -8,11 +8,9 @@
<h1 class="title">Event '{{ event.name }}' bearbeiten</h1> <h1 class="title">Event '{{ event.name }}' bearbeiten</h1>
{% else %} {% else %}
<form method="post" action="/events/new"> <form method="post" action="/events/new">
<h1 class="title">Neues Event anlegen für den {{ date.format("%d.%m.%Y") }}</h1> <h1 class="title">Neues Event anlegen für den {{ date|date_d }}</h1>
{% endif %} {% endif %}
<input type="hidden" name="date" value="{{ date }}">
{% if let Some(event) = event %} {% if let Some(event) = event %}
<div class="field is-horizontal"> <div class="field is-horizontal">
<div class="field-label"></div> <div class="field-label"></div>
@ -20,7 +18,7 @@
<div class="field is-grouped"> <div class="field is-grouped">
<div class="control"> <div class="control">
<button class="button is-warning" type="button" <button class="button is-warning" type="button"
hx-put="/events/{{ event.id }}/{% if event.canceled %}uncancel{% else %}cancel{% endif %}"> hx-put="/events/{{ event.id }}/{% if event.canceled %}uncancel{% else %}cancel{% endif %}">
<svg class="icon"> <svg class="icon">
<use href="/static/feather-sprite.svg#alert-circle" /> <use href="/static/feather-sprite.svg#alert-circle" />
</svg> </svg>
@ -32,8 +30,9 @@
<div class="control"> <div class="control">
{% let delete_disabled = amount_of_planned_posten > 0 || is_wachhabender_planned || {% let delete_disabled = amount_of_planned_posten > 0 || is_wachhabender_planned ||
is_fuehrungsassistent_planned %} is_fuehrungsassistent_planned %}
<button class="button is-danger" type="button" hx-delete="/events/{{ event.id }}" {{ <button class="button is-danger" type="button"
delete_disabled|cond_show("disabled") }} hx-trigger="confirmed"> hx-delete="/events/{{ event.id }}" {{ delete_disabled|cond_show("disabled") }}
hx-trigger="confirmed">
<svg class="icon"> <svg class="icon">
<use href="/static/feather-sprite.svg#x-circle" /> <use href="/static/feather-sprite.svg#x-circle" />
</svg> </svg>
@ -59,8 +58,9 @@
<div class="field-body"> <div class="field-body">
<div class="field"> <div class="field">
<div class="control"> <div class="control">
<input class="input" name="name" placeholder="Wave Gotik Treffen" required {% if let Some(event)=event <input class="input" name="name" placeholder="Wave Gotik Treffen" required
%} value="{{ event.name }}" {% endif %} {{ disabled|cond_show("disabled") }} /> {% if let Some(event)=event %} value="{{ event.name }}" {% endif %}
{{ disabled|cond_show("disabled") }} />
</div> </div>
</div> </div>
</div> </div>
@ -71,14 +71,17 @@
<label class="label">Startzeit - Endzeit</label> <label class="label">Startzeit - Endzeit</label>
</div> </div>
<div class="field-body"> <div class="field-body">
{% let f = "%H:%M" %}
<div class="field"> <div class="field">
<input class="input" type="time" id="from" name="from" required value="{% if let Some(event)=event <input class="input" type="datetime-local" id="from" name="from" required
%}{{ event.start_time.format(f) }}{% else %}00:00{% endif %}" {{ disabled|cond_show("disabled") }} /> value="{% if let Some(event)=event %}{{ event.start|dt_f }}{% else %}{{ date }}{% endif
%}"
{{ disabled|cond_show("disabled") }} min="{{ date }}T00:00" max="{{ date }}T23:59" />
</div> </div>
<div class="field"> <div class="field">
<input class="input" type="time" id="till" name="till" required value="{% if let Some(event)=event <input class="input" type="datetime-local" id="till" name="till" required
%}{{ event.end_time.format(f) }}{% else %}23:59{% endif %}" {{ disabled|cond_show("disabled") }} /> value="{% if let Some(event)=event %}{{ event.end|dt_f }}{% else %}{{ date }}{% endif %}"
{{ disabled|cond_show("disabled") }} min="{{ date }}T00:00"
max="{{ date.checked_add_days(Days::new(1)).unwrap() }}T23:59" />
</div> </div>
</div> </div>
</div> </div>
@ -94,7 +97,8 @@
<select name="location" required {{ disabled|cond_show("disabled") }}> <select name="location" required {{ disabled|cond_show("disabled") }}>
{% for location in locations %} {% for location in locations %}
<option value="{{ location.id }}" {% if let Some(event)=event %}{{ <option value="{{ location.id }}" {% if let Some(event)=event %}{{
(event.location_id==location.id)|cond_show("selected") }}{% endif %}>{{ location.name }}{% if (event.location_id==location.id)|cond_show("selected") }}{% endif %}>{{ location.name }}{%
if
user.role == Role::Admin %} - ({{ user.role == Role::Admin %} - ({{
location.area.as_ref().unwrap().name }}){% endif %} location.area.as_ref().unwrap().name }}){% endif %}
</option> </option>
@ -116,9 +120,10 @@
<div class="control"> <div class="control">
<label class="checkbox"> <label class="checkbox">
<input class="checkbox" type="checkbox" name="voluntarywachhabender" value="true" {{ <input class="checkbox" type="checkbox" name="voluntarywachhabender" value="true" {{
wh_disabled|cond_show("disabled")}} {% if let Some(event)=event %} {{ wh_disabled|cond_show("disabled")}} {% if let Some(event)=event %} {{
event.voluntary_wachhabender|cond_show("checked") }} {% endif %} {{ disabled|cond_show("disabled") event.voluntary_wachhabender|cond_show("checked") }} {% endif %} {{
}}> disabled|cond_show("disabled")
}}>
</label> </label>
</div> </div>
{% if wh_disabled %} {% if wh_disabled %}
@ -140,9 +145,9 @@
<div class="control"> <div class="control">
<label class="checkbox"> <label class="checkbox">
<input class="checkbox" type="checkbox" name="voluntaryfuehrungsassistent" value="true" {{ <input class="checkbox" type="checkbox" name="voluntaryfuehrungsassistent" value="true" {{
fa_disabled|cond_show("disabled") }} {% if let Some(event)=event %} {{ fa_disabled|cond_show("disabled") }} {% if let Some(event)=event %} {{
event.voluntary_fuehrungsassistent|cond_show("checked") }} {% endif %} {{ event.voluntary_fuehrungsassistent|cond_show("checked") }} {% endif %} {{
disabled|cond_show("disabled") }}> disabled|cond_show("disabled") }}>
</label> </label>
</div> </div>
{% if fa_disabled %} {% if fa_disabled %}
@ -163,9 +168,10 @@
{% let posten_planned = event.is_some() && amount_of_planned_posten > 0 %} {% let posten_planned = event.is_some() && amount_of_planned_posten > 0 %}
<div class="control"> <div class="control">
<input class="input" type="number" name="amount" <input class="input" type="number" name="amount"
min="{% if posten_planned %}{{ amount_of_planned_posten }}{% else %}0{% endif %}" max="100" required min="{% if posten_planned %}{{ amount_of_planned_posten }}{% else %}0{% endif %}" max="100"
{% if let Some(event)=event %} value="{{ event.amount_of_posten }}" {% endif %} {{ required
disabled|cond_show("disabled") }} /> {% if let Some(event)=event %} value="{{ event.amount_of_posten }}" {% endif %} {{
disabled|cond_show("disabled") }} />
</div> </div>
{% if posten_planned %} {% if posten_planned %}
<p class="help"> <p class="help">
@ -186,7 +192,7 @@
<div class="field"> <div class="field">
<div class="control"> <div class="control">
<input class="input" name="clothing" placeholder="Tuchuniform" required {% if let Some(event)=event %} <input class="input" name="clothing" placeholder="Tuchuniform" required {% if let Some(event)=event %}
value="{{ event.clothing }}" {% endif %} {{ disabled|cond_show("disabled") }} /> value="{{ event.clothing }}" {% endif %} {{ disabled|cond_show("disabled") }} />
</div> </div>
</div> </div>
</div> </div>
@ -200,7 +206,7 @@
<div class="field"> <div class="field">
<div class="control"> <div class="control">
<input class="input" name="note" {% if let Some(event)=event %} {{ event.note|insert_value }} {% endif <input class="input" name="note" {% if let Some(event)=event %} {{ event.note|insert_value }} {% endif
%} {{ disabled|cond_show("disabled") }} /> %} {{ disabled|cond_show("disabled") }} />
</div> </div>
</div> </div>
</div> </div>

View File

@ -15,11 +15,11 @@
</div> </div>
<div class="cell"> <div class="cell">
<p><b>Datum:</b> {{ event.date.format("%d.%m.%Y") }}</p> <p><b>Datum:</b> {{ event.start.format("%d.%m.%Y") }}</p>
</div> </div>
<div class="cell"> <div class="cell">
<p><b>Uhrzeit:</b> {{ event.start_time.format("%R") }} Uhr - {{ event.end_time.format("%R") }} Uhr</p> <p><b>Uhrzeit:</b> {{ event.start.format("%R") }} Uhr - {{ event.end.format("%d.%m.%Y %R") }} Uhr</p>
</div> </div>
<div class="cell is-col-span-2"> <div class="cell is-col-span-2">
@ -62,7 +62,7 @@
</div> </div>
<div class="control"> <div class="control">
<a class="button is-link is-light" hx-boost="true" href="/?date={{ event.date }}"> <a class="button is-link is-light" hx-boost="true" href="/?date={{ event.start.date() }}">
<svg class="icon"> <svg class="icon">
<use href="/static/feather-sprite.svg#arrow-left" /> <use href="/static/feather-sprite.svg#arrow-left" />
</svg> </svg>

View File

@ -106,7 +106,7 @@
{% endif %} {% endif %}
<div class="cell"> <div class="cell">
<p><b>Uhrzeit:</b> {{ event.start_time.format("%R") }} Uhr - {{ event.end_time.format("%R") }} Uhr</p> <p><b>Uhrzeit:</b> {{ event.start|dt_t }} Uhr - {{ event.end|dt_f }} Uhr</p>
</div> </div>
<div class="cell"> <div class="cell">