feat: WIP show foreign events in calendar

This commit is contained in:
Max Hohlfeld 2025-08-04 23:52:18 +02:00
parent 94143f54d7
commit d88fe2cd3a
11 changed files with 269 additions and 81 deletions

View File

@ -0,0 +1,34 @@
SELECT
event.id AS eventId,
event.startTimestamp,
event.endTimestamp,
event.name,
event.locationId,
event.voluntaryWachhabender,
event.fuehrungsassistentRequired,
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
assignment
JOIN availability ON
assignment.availabilityid = availability.id
JOIN event ON
assignment.eventid = "event".id
JOIN location ON
event.locationid = location.id
JOIN clothing ON
event.clothing = clothing.id
WHERE
userid = $1
AND event.starttimestamp::date >= $2
AND event.starttimestamp::date <= $3
AND location.areaId != $4
ORDER BY
event.starttimestamp;

View File

@ -0,0 +1,57 @@
SELECT DISTINCT
event.id AS eventId,
event.startTimestamp,
event.endTimestamp,
event.name,
event.locationId,
event.voluntaryWachhabender,
event.fuehrungsassistentRequired,
event.amountOfPosten,
event.clothing,
event.canceled,
event.note,
location.id,
location.name AS locationName,
location.areaId AS locationAreaId,
area.name AS areaName,
clothing.id AS clothingId,
clothing.name AS clothingName,
ARRAY (
SELECT
ROW (user_.name,
assignment.function) ::simpleAssignment
FROM
ASSIGNMENT
JOIN availability ON
assignment.availabilityid = availability.id
JOIN user_ ON
availability.userid = user_.id
WHERE
assignment.eventId = event.id) AS "assignments: Vec<SimpleAssignment>",
ARRAY (
SELECT
vehicle.radiocallname || ' - ' || vehicle.station
FROM
vehicleassignment
JOIN vehicle ON
vehicleassignment.vehicleId = vehicle.id
WHERE
vehicleassignment.eventId = event.id) AS vehicles
FROM
EVENT
JOIN LOCATION ON
event.locationId = location.id
JOIN AREA ON
location.areaId = area.id
JOIN clothing ON
event.clothing = clothing.id
LEFT JOIN ASSIGNMENT ON
event.id = assignment.eventid
LEFT JOIN availability ON
assignment.availabilityid = availability.id
WHERE
event.starttimestamp::date = $1
AND (
location.areaId = $2
OR availability.userId = $3
)

View File

@ -63,7 +63,7 @@ fn availability_user_inside_event_area(
let user = availability.user.as_ref().unwrap(); let user = availability.user.as_ref().unwrap();
let location = event.location.as_ref().unwrap(); let location = event.location.as_ref().unwrap();
if user.area_id != location.area_id { if user.area_id != location.area_id && !availability.cross_areal {
return Err(AsyncValidateError::new( return Err(AsyncValidateError::new(
"Nutzer der Verfügbarkeit ist nicht im gleichen Bereich wie der Ort der Veranstaltung.", "Nutzer der Verfügbarkeit ist nicht im gleichen Bereich wie der Ort der Veranstaltung.",
)); ));

View File

@ -1,4 +1,4 @@
use chrono::{Days, NaiveDate, NaiveDateTime}; use chrono::{Days, NaiveDateTime};
use sqlx::PgPool; use sqlx::PgPool;
use super::Availability; use super::Availability;
@ -154,7 +154,7 @@ pub fn find_free_date_time_slots(
#[cfg(feature = "test-helpers")] #[cfg(feature = "test-helpers")]
impl AvailabilityChangeset { impl AvailabilityChangeset {
pub fn create_for_test( pub fn create_for_test(
date: &NaiveDate, date: &chrono::NaiveDate,
start_hour: u32, start_hour: u32,
end_hour: u32, end_hour: u32,
) -> AvailabilityChangeset { ) -> AvailabilityChangeset {

View File

@ -1,5 +1,5 @@
use chrono::{NaiveDate, NaiveDateTime}; use chrono::{NaiveDate, NaiveDateTime};
use sqlx::{PgPool, query}; use sqlx::{PgPool, query, query_file};
use super::{Clothing, EventChangeset, Location, Result}; use super::{Clothing, EventChangeset, Location, Result};
@ -163,6 +163,51 @@ impl Event {
Ok(events) Ok(events)
} }
pub async fn read_all_by_date_range_and_assigned_user_in_other_area(
pool: &PgPool,
date_range: (&NaiveDate, &NaiveDate),
user_id: i32,
area_to_ignore: i32,
) -> Result<Vec<Event>> {
let records = query_file!(
"sql/event/read_all_by_date_range_and_assigned_user_in_other_area.sql",
user_id,
date_range.0,
date_range.1,
area_to_ignore
)
.fetch_all(pool)
.await?;
let events = records
.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,
fuehrungsassistent_required: record.fuehrungsassistentrequired,
amount_of_posten: record.amountofposten,
clothing: Clothing {
id: record.clothingid,
name: record.clothingname.clone(),
},
canceled: record.canceled,
note: record.note.clone(),
})
.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#"

View File

@ -0,0 +1,90 @@
use chrono::NaiveDate;
use sqlx::{PgPool, query_file};
use super::{Area, Clothing, Event, Function, Location, Result, SimpleAssignment};
pub struct EventForCalendar {
pub event: Event,
pub assignments: Vec<SimpleAssignment>,
pub vehicle_assignments: Vec<String>,
}
impl EventForCalendar {
pub async fn read_all_by_date_and_area_and_user(
pool: &PgPool,
date: &NaiveDate,
area: i32,
user: i32,
) -> Result<Vec<EventForCalendar>> {
let records = query_file!(
"sql/event_for_calendar/read_all_by_date_and_area_and_user.sql",
date,
area,
user
)
.fetch_all(pool)
.await?;
let events = records
.iter()
.map(|record| EventForCalendar {
event: 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: Some(Area {
id: record.locationareaid,
name: record.areaname.clone(),
}),
}),
voluntary_wachhabender: record.voluntarywachhabender,
fuehrungsassistent_required: record.fuehrungsassistentrequired,
amount_of_posten: record.amountofposten,
clothing: Clothing {
id: record.clothingid,
name: record.clothingname.clone(),
},
canceled: record.canceled,
note: record.note.clone(),
},
assignments: record
.assignments
.as_ref()
.and_then(|f| Some(f.to_vec()))
.unwrap_or_default(),
vehicle_assignments: record
.vehicles
.as_ref()
.and_then(|f| Some(f.to_vec()))
.unwrap_or_default(),
})
.collect();
Ok(events)
}
pub fn get_wachhabender(&self) -> Option<&SimpleAssignment> {
self.assignments
.iter()
.find(|a| a.function == Function::Wachhabender)
}
pub fn get_fuehrungsassistent(&self) -> Option<&SimpleAssignment> {
self.assignments
.iter()
.find(|a| a.function == Function::Fuehrungsassistent)
}
pub fn get_posten(&self) -> Vec<&SimpleAssignment> {
self.assignments
.iter()
.filter(|a| a.function == Function::Posten)
.collect()
}
}

View File

@ -19,7 +19,7 @@ pub struct ExportEventRow {
pub vehicles: Vec<String>, pub vehicles: Vec<String>,
} }
#[derive(Debug, sqlx::Type)] #[derive(Debug, sqlx::Type, Clone)]
#[sqlx(type_name = "function", no_pg_array)] #[sqlx(type_name = "function", no_pg_array)]
pub struct SimpleAssignment { pub struct SimpleAssignment {
pub name: String, pub name: String,

View File

@ -7,6 +7,7 @@ mod availability_changeset;
mod clothing; mod clothing;
mod event; mod event;
mod event_changeset; mod event_changeset;
mod event_for_calendar;
mod export_event_row; mod export_event_row;
mod function; mod function;
mod location; mod location;
@ -30,6 +31,7 @@ pub use availability_changeset::{
pub use clothing::Clothing; pub use clothing::Clothing;
pub use event::Event; pub use event::Event;
pub use event_changeset::{EventChangeset, EventContext}; pub use event_changeset::{EventChangeset, EventContext};
pub use event_for_calendar::EventForCalendar;
pub use export_event_row::{ExportEventRow, SimpleAssignment}; pub use export_event_row::{ExportEventRow, SimpleAssignment};
pub use function::Function; pub use function::Function;
pub use location::Location; pub use location::Location;

View File

@ -7,14 +7,13 @@ 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::{
find_free_date_time_slots, Area, Assignment, Availability, Event, Function, Role, User, Vehicle, find_free_date_time_slots, Area, Availability, EventForCalendar, Role, User,
}; };
#[derive(Deserialize)] #[derive(Deserialize)]
@ -31,13 +30,7 @@ struct CalendarTemplate {
date: NaiveDate, date: NaiveDate,
selected_area: Option<i32>, selected_area: Option<i32>,
areas: Vec<Area>, areas: Vec<Area>,
events_and_assignments: Vec<( events: Vec<EventForCalendar>,
Event,
Vec<String>,
Option<String>,
Option<String>,
Vec<Vehicle>,
)>,
availabilities: Vec<Availability>, availabilities: Vec<Availability>,
} }
@ -83,66 +76,13 @@ async fn get(
// !only_one_availability_exists_and_is_whole_day(&availabilities_from_user), // !only_one_availability_exists_and_is_whole_day(&availabilities_from_user),
// !find_free_time_slots(&availabilities_from_user).is_empty()); // !find_free_time_slots(&availabilities_from_user).is_empty());
let mut events_and_assignments = Vec::new(); let events = EventForCalendar::read_all_by_date_and_area_and_user(
for e in Event::read_all_by_date_and_area_including_location(
pool.get_ref(), pool.get_ref(),
date, &date,
query.area.unwrap_or(user.area_id), query.area.unwrap_or(user.area_id),
user.id,
) )
.await? .await?;
.into_iter()
{
let assignments = Assignment::read_all_by_event(pool.get_ref(), e.id).await?;
let (posten, rest): (Vec<Assignment>, Vec<Assignment>) = assignments
.into_iter()
.partition(|a| a.function == Function::Posten);
let (wachhabender, fuehrungsassistent): (Vec<Assignment>, Vec<Assignment>) = rest
.into_iter()
.partition(|a| a.function == Function::Wachhabender);
let (assigned_vehicle, _) = generate_vehicles_assigned_and_available(&pool, &e).await?;
events_and_assignments.push((
e,
posten
.into_iter()
.map(|p| {
availabilities
.iter()
.find(|a| a.id == p.availability_id)
.unwrap()
.user
.as_ref()
.unwrap()
.name
.clone()
})
.collect(),
fuehrungsassistent.first().map(|fa| {
availabilities
.iter()
.find(|a| a.id == fa.availability_id)
.unwrap()
.user
.as_ref()
.unwrap()
.name
.clone()
}),
wachhabender.first().map(|wh| {
availabilities
.iter()
.find(|a| a.id == wh.availability_id)
.unwrap()
.user
.as_ref()
.unwrap()
.name
.clone()
}),
assigned_vehicle,
));
}
let template = CalendarTemplate { let template = CalendarTemplate {
user: user.into_inner(), user: user.into_inner(),
@ -150,7 +90,7 @@ async fn get(
date, date,
selected_area, selected_area,
areas, areas,
events_and_assignments, events,
availabilities, availabilities,
}; };

View File

@ -36,13 +36,24 @@ async fn get(
let next_month = today.checked_add_months(Months::new(1)).unwrap(); let next_month = today.checked_add_months(Months::new(1)).unwrap();
let date_range = (&today, &next_month); let date_range = (&today, &next_month);
let events = Event::read_all_by_daterange_and_area_including_location( let mut events = Event::read_all_by_daterange_and_area_including_location(
pool.get_ref(), pool.get_ref(),
date_range, date_range,
user.area_id, user.area_id,
) )
.await?; .await?;
let mut foreign_events = Event::read_all_by_date_range_and_assigned_user_in_other_area(
pool.get_ref(),
date_range,
user.id,
user.area_id,
)
.await?;
events.append(&mut foreign_events);
events.sort_by(|a, b| a.start.date().cmp(&b.start.date()));
let availabilities = let availabilities =
Availability::read_all_by_user_and_daterange(pool.get_ref(), user.id, date_range).await?; Availability::read_all_by_user_and_daterange(pool.get_ref(), user.id, date_range).await?;

View File

@ -146,12 +146,18 @@
{% endif %} {% endif %}
</div> </div>
{% if events_and_assignments.len() == 0 %} {% if events.len() == 0 %}
<div class="notification"> <div class="notification">
Keine Veranstaltungen geplant. Keine Veranstaltungen geplant.
</div> </div>
{% else %} {% else %}
{% for (event, posten, fuehrungsassistent, wachhabender, vehicle) in events_and_assignments %} {% for calendar_event in events %}
{%- let event = calendar_event.event -%}
{%- let location = calendar_event.event.location.as_ref().unwrap() -%}
{%- let wachhabender = calendar_event.get_wachhabender() -%}
{%- let fuehrungsassistent = calendar_event.get_fuehrungsassistent() -%}
{%- let posten = calendar_event.get_posten() -%}
{%- let vehicle = calendar_event.vehicle_assignments -%}
<div class="box"> <div class="box">
<div class="fixed-grid has-1-cols-mobile"> <div class="fixed-grid has-1-cols-mobile">
<div class="grid content"> <div class="grid content">
@ -159,7 +165,7 @@
<h5 class="title is-5">{{ event.name }}</h5> <h5 class="title is-5">{{ event.name }}</h5>
</div> </div>
{% if user.role == Role::AreaManager || user.role == Role::Admin %} {% if (user.role == Role::AreaManager && location.area_id == user.area_id) || user.role == Role::Admin %}
<div class="cell is-narrow buttons is-justify-content-end mb-0"> <div class="cell is-narrow buttons is-justify-content-end mb-0">
<a href="/events/{{ event.id }}/plan" hx-boost="true" class="button is-link is-light"> <a href="/events/{{ event.id }}/plan" hx-boost="true" class="button is-link is-light">
<svg class="icon"> <svg class="icon">
@ -188,7 +194,10 @@
</div> </div>
<div class="cell"> <div class="cell">
<p><b>Veranstaltungsort:</b> {{ event.location.as_ref().unwrap().name }}</p> <p>
<b>Veranstaltungsort:</b> {{ location.name }} {% if location.area_id != user.area_id %}(Fremdbereich {{
location.area.as_ref().unwrap().name }}){% endif %}
</p>
</div> </div>
<div class="cell"> <div class="cell">
@ -222,19 +231,19 @@
{% if let Some(wh) = wachhabender %} {% if let Some(wh) = wachhabender %}
<div class="cell"> <div class="cell">
<p><b>Wachhabender geplant:</b> {{ wh }}</p> <p><b>Wachhabender geplant:</b> {{ wh.name }}</p>
</div> </div>
{% endif %} {% endif %}
{% if let Some(fa) = fuehrungsassistent %} {% if let Some(fa) = fuehrungsassistent %}
<div class="cell"> <div class="cell">
<p><b>Führungsassistent geplant:</b> {{ fa }}</p> <p><b>Führungsassistent geplant:</b> {{ fa.name }}</p>
</div> </div>
{% endif %} {% endif %}
{% if posten.len() > 0 %} {% if posten.len() > 0 %}
<div class="cell is-col-span-2"> <div class="cell is-col-span-2">
<p><b>Posten:</b> {{ posten.join(", ")}}</p> <p><b>Posten:</b> {% for p in posten -%}{{ p.name }}{% if !loop.last %}, {% endif %}{% endfor %}</p>
</div> </div>
{% endif %} {% endif %}
@ -242,7 +251,7 @@
<div class="cell is-col-span-2"> <div class="cell is-col-span-2">
<b>Fahrzeuge:</b> <b>Fahrzeuge:</b>
{% for v in vehicle %} {% for v in vehicle %}
<span class="tag is-link"> {{ v.radio_call_name }} - {{ v.station }}</span> <span class="tag is-link"> {{ v }}</span>
{% endfor %} {% endfor %}
</div> </div>
{% endif %} {% endif %}