Compare commits
3 Commits
b42540ac2f
...
38a30e1898
Author | SHA1 | Date | |
---|---|---|---|
38a30e1898 | |||
2ec200831f | |||
d1e067407b |
114
db/.sqlx/query-10b4b80f351b66ac5e778a3031288ac5dc66efd0a66b38b7e30f4c954df91bdf.json
generated
Normal file
114
db/.sqlx/query-10b4b80f351b66ac5e778a3031288ac5dc66efd0a66b38b7e30f4c954df91bdf.json
generated
Normal 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"
|
||||||
|
}
|
48
db/.sqlx/query-f60053118df6a791d31fa258ee3737881f8f97ca41cbebd92eb22c967292d2ee.json
generated
Normal file
48
db/.sqlx/query-f60053118df6a791d31fa258ee3737881f8f97ca41cbebd92eb22c967292d2ee.json
generated
Normal 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"
|
||||||
|
}
|
@ -341,6 +341,46 @@ impl Availability {
|
|||||||
Ok(availabilities)
|
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(
|
pub async fn find_adjacent_by_time_for_user(
|
||||||
pool: &PgPool,
|
pool: &PgPool,
|
||||||
start: &NaiveDateTime,
|
start: &NaiveDateTime,
|
||||||
|
@ -2,9 +2,11 @@ use chrono::{Days, NaiveDateTime};
|
|||||||
use sqlx::PgPool;
|
use sqlx::PgPool;
|
||||||
|
|
||||||
use super::Availability;
|
use super::Availability;
|
||||||
use crate::{validation::{
|
use crate::{
|
||||||
start_date_time_lies_before_end_date_time, AsyncValidate, AsyncValidateError
|
END_OF_DAY, START_OF_DAY,
|
||||||
}, END_OF_DAY, START_OF_DAY};
|
models::Assignment,
|
||||||
|
validation::{AsyncValidate, AsyncValidateError, start_date_time_lies_before_end_date_time},
|
||||||
|
};
|
||||||
|
|
||||||
pub struct AvailabilityChangeset {
|
pub struct AvailabilityChangeset {
|
||||||
pub time: (NaiveDateTime, NaiveDateTime),
|
pub time: (NaiveDateTime, NaiveDateTime),
|
||||||
@ -14,7 +16,7 @@ pub struct AvailabilityChangeset {
|
|||||||
pub struct AvailabilityContext<'a> {
|
pub struct AvailabilityContext<'a> {
|
||||||
pub pool: &'a PgPool,
|
pub pool: &'a PgPool,
|
||||||
pub user_id: i32,
|
pub user_id: i32,
|
||||||
pub availability_to_get_edited: Option<i32>,
|
pub availability: Option<i32>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl<'a> AsyncValidate<'a> for AvailabilityChangeset {
|
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())
|
Availability::read_by_user_and_date(context.pool, context.user_id, &self.time.0.date())
|
||||||
.await?;
|
.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
|
existing_availabilities = existing_availabilities
|
||||||
.into_iter()
|
.into_iter()
|
||||||
.filter(|a| a.id != existing)
|
.filter(|a| a.id != availability)
|
||||||
.collect();
|
.collect();
|
||||||
|
|
||||||
|
time_is_not_already_assigned(&self.time, availability, context.pool).await?;
|
||||||
}
|
}
|
||||||
|
|
||||||
time_is_not_already_made_available(&self.time, &existing_availabilities)?;
|
if !existing_availabilities.is_empty() {
|
||||||
start_date_time_lies_before_end_date_time(&self.time.0, &self.time.1)?;
|
time_is_not_already_made_available(&self.time, &existing_availabilities)?;
|
||||||
|
}
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn time_is_not_already_made_available(
|
fn time_is_not_already_made_available(
|
||||||
value: &(NaiveDateTime, NaiveDateTime),
|
(start, end): &(NaiveDateTime, NaiveDateTime),
|
||||||
existing_availabilities: &Vec<Availability>,
|
existing_availabilities: &Vec<Availability>,
|
||||||
) -> Result<(), AsyncValidateError> {
|
) -> Result<(), AsyncValidateError> {
|
||||||
if existing_availabilities.is_empty() {
|
|
||||||
return Ok(());
|
|
||||||
}
|
|
||||||
|
|
||||||
let free_slots = find_free_date_time_slots(existing_availabilities);
|
let free_slots = find_free_date_time_slots(existing_availabilities);
|
||||||
|
|
||||||
if free_slots.is_empty() {
|
if free_slots.is_empty() {
|
||||||
return Err(AsyncValidateError::new(
|
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_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 <= value.1 && s.1 >= value.1);
|
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(
|
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(())
|
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(
|
pub fn find_free_date_time_slots(
|
||||||
availabilities: &[Availability],
|
availabilities: &[Availability],
|
||||||
) -> Vec<(NaiveDateTime, NaiveDateTime)> {
|
) -> Vec<(NaiveDateTime, NaiveDateTime)> {
|
||||||
|
@ -95,6 +95,73 @@ impl Event {
|
|||||||
Ok(events)
|
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>> {
|
pub async fn read_by_id_including_location(pool: &PgPool, id: i32) -> Result<Option<Event>> {
|
||||||
let record = query!(
|
let record = query!(
|
||||||
r#"
|
r#"
|
||||||
|
@ -5,8 +5,8 @@ mod validation_trait;
|
|||||||
use chrono::NaiveDateTime;
|
use chrono::NaiveDateTime;
|
||||||
pub use email::email_is_valid;
|
pub use email::email_is_valid;
|
||||||
pub use error::AsyncValidateError;
|
pub use error::AsyncValidateError;
|
||||||
pub use validation_trait::AsyncValidate;
|
|
||||||
use sqlx::PgPool;
|
use sqlx::PgPool;
|
||||||
|
pub use validation_trait::AsyncValidate;
|
||||||
|
|
||||||
pub struct DbContext<'a> {
|
pub struct DbContext<'a> {
|
||||||
pub pool: &'a PgPool,
|
pub pool: &'a PgPool,
|
||||||
@ -24,7 +24,7 @@ pub fn start_date_time_lies_before_end_date_time(
|
|||||||
) -> Result<(), AsyncValidateError> {
|
) -> Result<(), AsyncValidateError> {
|
||||||
if start >= end {
|
if start >= end {
|
||||||
return Err(AsyncValidateError::new(
|
return Err(AsyncValidateError::new(
|
||||||
"endtime can't lie before starttime",
|
"Ende kann nicht vor dem Beginn liegen.",
|
||||||
));
|
));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
158
web/src/endpoints/availability/get_calendar.rs
Normal file
158
web/src/endpoints/availability/get_calendar.rs
Normal 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()?)
|
||||||
|
}
|
@ -1,43 +1,23 @@
|
|||||||
use actix_web::{web, HttpResponse, Responder};
|
use actix_web::{web, Responder};
|
||||||
use askama::Template;
|
use askama::Template;
|
||||||
use chrono::{NaiveDate, Utc};
|
use chrono::{Months, Utc};
|
||||||
use serde::Deserialize;
|
|
||||||
use sqlx::PgPool;
|
use sqlx::PgPool;
|
||||||
|
|
||||||
use crate::{
|
use crate::{
|
||||||
filters,
|
filters,
|
||||||
utils::{
|
utils::{
|
||||||
event_planning_template::generate_vehicles_assigned_and_available,
|
|
||||||
ApplicationError,
|
ApplicationError,
|
||||||
DateTimeFormat::{DayMonthYearHourMinute, HourMinute, WeekdayDayMonthYear},
|
DateTimeFormat::{DayMonthYearHourMinute, HourMinute, WeekdayDayMonthYear},
|
||||||
TemplateResponse,
|
TemplateResponse,
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
use brass_db::models::{
|
use brass_db::models::{Assignment, Availability, Event, Function, Role, User};
|
||||||
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)]
|
#[derive(Template)]
|
||||||
#[template(path = "index.html")]
|
#[template(path = "overview.html")]
|
||||||
struct CalendarTemplate {
|
struct OverviewTemplate {
|
||||||
user: User,
|
user: User,
|
||||||
user_can_create_availability: bool,
|
events: Vec<Event>,
|
||||||
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>,
|
availabilities: Vec<Availability>,
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -45,112 +25,24 @@ struct CalendarTemplate {
|
|||||||
async fn get(
|
async fn get(
|
||||||
user: web::ReqData<User>,
|
user: web::ReqData<User>,
|
||||||
pool: web::Data<PgPool>,
|
pool: web::Data<PgPool>,
|
||||||
query: web::Query<CalendarQuery>,
|
|
||||||
) -> Result<impl Responder, ApplicationError> {
|
) -> Result<impl Responder, ApplicationError> {
|
||||||
let date = match query.date {
|
let today = Utc::now().date_naive();
|
||||||
Some(given_date) => given_date,
|
let next_month = today.checked_add_months(Months::new(1)).unwrap();
|
||||||
None => Utc::now().date_naive(),
|
|
||||||
};
|
|
||||||
|
|
||||||
let areas = Area::read_all(pool.get_ref()).await?;
|
let events = Event::read_all_by_daterange_and_area_including_location(
|
||||||
|
|
||||||
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(),
|
pool.get_ref(),
|
||||||
date,
|
(&today, &next_month),
|
||||||
query.area.unwrap_or(user.area_id),
|
user.area_id,
|
||||||
)
|
)
|
||||||
.await?;
|
.await?;
|
||||||
|
|
||||||
let availabilities_from_user =
|
let availabilities =
|
||||||
Availability::read_by_user_and_date(pool.get_ref(), user.id, &date).await?;
|
Availability::read_by_user_and_daterange(pool.get_ref(), user.id, (&today, &next_month))
|
||||||
//println!("{availabilities_from_user:#?}");
|
.await?;
|
||||||
|
|
||||||
let user_can_create_availability = availabilities_from_user.is_empty()
|
let template = OverviewTemplate {
|
||||||
|| !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: user.into_inner(),
|
||||||
user_can_create_availability,
|
events,
|
||||||
date,
|
|
||||||
selected_area,
|
|
||||||
areas,
|
|
||||||
events_and_assignments,
|
|
||||||
availabilities,
|
availabilities,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -9,6 +9,7 @@ use crate::{
|
|||||||
use brass_db::models::{Role, User};
|
use brass_db::models::{Role, User};
|
||||||
|
|
||||||
pub mod delete;
|
pub mod delete;
|
||||||
|
pub mod get_calendar;
|
||||||
pub mod get_new;
|
pub mod get_new;
|
||||||
pub mod get_overview;
|
pub mod get_overview;
|
||||||
pub mod get_update;
|
pub mod get_update;
|
||||||
|
@ -1,4 +1,5 @@
|
|||||||
use actix_web::{http::header::LOCATION, web, HttpResponse, Responder};
|
use actix_web::{http::header::LOCATION, web, HttpResponse, Responder};
|
||||||
|
use maud::html;
|
||||||
use sqlx::PgPool;
|
use sqlx::PgPool;
|
||||||
|
|
||||||
use crate::{
|
use crate::{
|
||||||
@ -22,7 +23,7 @@ pub async fn post(
|
|||||||
let context = AvailabilityContext {
|
let context = AvailabilityContext {
|
||||||
pool: pool.get_ref(),
|
pool: pool.get_ref(),
|
||||||
user_id: user.id,
|
user_id: user.id,
|
||||||
availability_to_get_edited: None,
|
availability: None,
|
||||||
};
|
};
|
||||||
|
|
||||||
let mut changeset = AvailabilityChangeset {
|
let mut changeset = AvailabilityChangeset {
|
||||||
@ -31,7 +32,13 @@ pub async fn post(
|
|||||||
};
|
};
|
||||||
|
|
||||||
if let Err(e) = changeset.validate_with_context(&context).await {
|
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) =
|
if let Some(a) =
|
||||||
|
@ -1,4 +1,5 @@
|
|||||||
use actix_web::{http::header::LOCATION, web, HttpResponse, Responder};
|
use actix_web::{http::header::LOCATION, web, HttpResponse, Responder};
|
||||||
|
use maud::html;
|
||||||
use sqlx::PgPool;
|
use sqlx::PgPool;
|
||||||
|
|
||||||
use crate::{
|
use crate::{
|
||||||
@ -31,7 +32,7 @@ pub async fn post(
|
|||||||
let context = AvailabilityContext {
|
let context = AvailabilityContext {
|
||||||
pool: pool.get_ref(),
|
pool: pool.get_ref(),
|
||||||
user_id: user.id,
|
user_id: user.id,
|
||||||
availability_to_get_edited: Some(availability.id),
|
availability: Some(availability.id),
|
||||||
};
|
};
|
||||||
|
|
||||||
let mut changeset = AvailabilityChangeset {
|
let mut changeset = AvailabilityChangeset {
|
||||||
@ -40,7 +41,13 @@ pub async fn post(
|
|||||||
};
|
};
|
||||||
|
|
||||||
if let Err(e) = changeset.validate_with_context(&context).await {
|
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(
|
if let Some(a) = Availability::find_adjacent_by_time_for_user(
|
||||||
|
@ -56,10 +56,11 @@ pub fn init(cfg: &mut ServiceConfig) {
|
|||||||
|
|
||||||
cfg.service(availability::delete::delete);
|
cfg.service(availability::delete::delete);
|
||||||
cfg.service(availability::get_new::get);
|
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::get_update::get);
|
||||||
cfg.service(availability::post_new::post);
|
cfg.service(availability::post_new::post);
|
||||||
cfg.service(availability::post_update::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_cancel);
|
||||||
cfg.service(events::put_cancelation::put_uncancel);
|
cfg.service(events::put_cancelation::put_uncancel);
|
||||||
|
@ -4,7 +4,8 @@
|
|||||||
<section class="section">
|
<section class="section">
|
||||||
<div class="container">
|
<div class="container">
|
||||||
{% let is_edit = id.is_some() %}
|
{% 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 {{
|
<h1 class="title">{% if is_edit %}Bearbeite{% else %}Neue{% endif %} Vefügbarkeit für {{
|
||||||
date|fmt_date(WeekdayDayMonthYear) }}</h1>
|
date|fmt_date(WeekdayDayMonthYear) }}</h1>
|
||||||
|
|
||||||
@ -18,7 +19,7 @@
|
|||||||
<div class="field-body">
|
<div class="field-body">
|
||||||
<div class="field">
|
<div class="field">
|
||||||
<input class="input" type="time" name="starttime" required {{ start|insert_time_value|safe }}
|
<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 %}
|
{% if slot_suggestions.len() > 0 %}
|
||||||
<p class="help">noch mögliche Zeiträume:</p>
|
<p class="help">noch mögliche Zeiträume:</p>
|
||||||
<div class="tags help">
|
<div class="tags help">
|
||||||
@ -32,7 +33,7 @@
|
|||||||
</div>
|
</div>
|
||||||
<div class="field">
|
<div class="field">
|
||||||
<input class="input" type="time" name="endtime" required {{ end|insert_time_value|safe }}
|
<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 if (value of the previous <input/>) is greater than (value of me)
|
||||||
then set the value of #enddate to "{{ datetomorrow }}"
|
then set the value of #enddate to "{{ datetomorrow }}"
|
||||||
then put "{{ datetomorrow|fmt_date(WeekdayDayMonth) }}" into #ed
|
then put "{{ datetomorrow|fmt_date(WeekdayDayMonth) }}" into #ed
|
||||||
@ -53,13 +54,15 @@
|
|||||||
<label class="radio">
|
<label class="radio">
|
||||||
<input type="radio" name="isovernight" {{ is_overnight|invert|ref|cond_show("checked")|safe }}
|
<input type="radio" name="isovernight" {{ is_overnight|invert|ref|cond_show("checked")|safe }}
|
||||||
_='on click set the value of #enddate to "{{ date }}"
|
_='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
|
am selben Tag
|
||||||
</label>
|
</label>
|
||||||
<label class="radio ml-3">
|
<label class="radio ml-3">
|
||||||
<input type="radio" id="radionextday" name="isovernight" {{ is_overnight|cond_show("checked")|safe }}
|
<input type="radio" id="radionextday" name="isovernight" {{ is_overnight|cond_show("checked")|safe }}
|
||||||
_='on click set the value of #enddate to "{{ datetomorrow }}"
|
_='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
|
am Tag darauf
|
||||||
</label>
|
</label>
|
||||||
</div>
|
</div>
|
||||||
@ -90,6 +93,7 @@
|
|||||||
<span id="et">{{ end_time|fmt_time(HourMinute) }}</span>
|
<span id="et">{{ end_time|fmt_time(HourMinute) }}</span>
|
||||||
Uhr
|
Uhr
|
||||||
</p>
|
</p>
|
||||||
|
<p id="error" class="help is-danger"></p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@ -99,7 +103,7 @@
|
|||||||
<div class="field-body">
|
<div class="field-body">
|
||||||
<div class="field is-grouped">
|
<div class="field is-grouped">
|
||||||
<div class="control">
|
<div class="control">
|
||||||
<button class="button is-success">
|
<button class="button is-success" _="on click put '' into #error">
|
||||||
<svg class="icon">
|
<svg class="icon">
|
||||||
<use href="/static/feather-sprite.svg#check-circle" />
|
<use href="/static/feather-sprite.svg#check-circle" />
|
||||||
</svg>
|
</svg>
|
||||||
|
@ -18,6 +18,10 @@
|
|||||||
<div class="navbar-menu" id="navMenu">
|
<div class="navbar-menu" id="navMenu">
|
||||||
<div hx-boost="true" class="navbar-start">
|
<div hx-boost="true" class="navbar-start">
|
||||||
<a href="/" class="navbar-item">
|
<a href="/" class="navbar-item">
|
||||||
|
Übersicht
|
||||||
|
</a>
|
||||||
|
|
||||||
|
<a href="/calendar" class="navbar-item">
|
||||||
Kalender
|
Kalender
|
||||||
</a>
|
</a>
|
||||||
|
|
||||||
|
9
web/templates/overview.html
Normal file
9
web/templates/overview.html
Normal file
@ -0,0 +1,9 @@
|
|||||||
|
{% extends "nav.html" %}
|
||||||
|
|
||||||
|
{% block content %}
|
||||||
|
<section id="progress" class="section">
|
||||||
|
<div class="container">
|
||||||
|
Übersicht
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
{% endblock %}
|
@ -3,7 +3,8 @@
|
|||||||
{% block content %}
|
{% block content %}
|
||||||
<section class="section">
|
<section class="section">
|
||||||
<div class="container">
|
<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">
|
<h1 class="title">
|
||||||
{% if let Some(name) = name %}Nutzer '{{ name }}' bearbeiten{% else %}Neuen Nutzer anlegen{% endif %}
|
{% if let Some(name) = name %}Nutzer '{{ name }}' bearbeiten{% else %}Neuen Nutzer anlegen{% endif %}
|
||||||
</h1>
|
</h1>
|
||||||
|
Loading…
x
Reference in New Issue
Block a user