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 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(
"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 super::Availability;
@ -154,7 +154,7 @@ pub fn find_free_date_time_slots(
#[cfg(feature = "test-helpers")]
impl AvailabilityChangeset {
pub fn create_for_test(
date: &NaiveDate,
date: &chrono::NaiveDate,
start_hour: u32,
end_hour: u32,
) -> AvailabilityChangeset {

View File

@ -1,5 +1,5 @@
use chrono::{NaiveDate, NaiveDateTime};
use sqlx::{PgPool, query};
use sqlx::{PgPool, query, query_file};
use super::{Clothing, EventChangeset, Location, Result};
@ -163,6 +163,51 @@ impl Event {
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>> {
let record = query!(
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>,
}
#[derive(Debug, sqlx::Type)]
#[derive(Debug, sqlx::Type, Clone)]
#[sqlx(type_name = "function", no_pg_array)]
pub struct SimpleAssignment {
pub name: String,

View File

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

View File

@ -7,14 +7,13 @@ 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,
find_free_date_time_slots, Area, Availability, EventForCalendar, Role, User,
};
#[derive(Deserialize)]
@ -31,13 +30,7 @@ struct CalendarTemplate {
date: NaiveDate,
selected_area: Option<i32>,
areas: Vec<Area>,
events_and_assignments: Vec<(
Event,
Vec<String>,
Option<String>,
Option<String>,
Vec<Vehicle>,
)>,
events: Vec<EventForCalendar>,
availabilities: Vec<Availability>,
}
@ -83,66 +76,13 @@ async fn get(
// !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(
let events = EventForCalendar::read_all_by_date_and_area_and_user(
pool.get_ref(),
date,
&date,
query.area.unwrap_or(user.area_id),
user.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,
));
}
.await?;
let template = CalendarTemplate {
user: user.into_inner(),
@ -150,7 +90,7 @@ async fn get(
date,
selected_area,
areas,
events_and_assignments,
events,
availabilities,
};

View File

@ -36,13 +36,24 @@ async fn get(
let next_month = today.checked_add_months(Months::new(1)).unwrap();
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(),
date_range,
user.area_id,
)
.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 =
Availability::read_all_by_user_and_daterange(pool.get_ref(), user.id, date_range).await?;

View File

@ -146,12 +146,18 @@
{% endif %}
</div>
{% if events_and_assignments.len() == 0 %}
{% if events.len() == 0 %}
<div class="notification">
Keine Veranstaltungen geplant.
</div>
{% 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="fixed-grid has-1-cols-mobile">
<div class="grid content">
@ -159,7 +165,7 @@
<h5 class="title is-5">{{ event.name }}</h5>
</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">
<a href="/events/{{ event.id }}/plan" hx-boost="true" class="button is-link is-light">
<svg class="icon">
@ -188,7 +194,10 @@
</div>
<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 class="cell">
@ -222,19 +231,19 @@
{% if let Some(wh) = wachhabender %}
<div class="cell">
<p><b>Wachhabender geplant:</b> {{ wh }}</p>
<p><b>Wachhabender geplant:</b> {{ wh.name }}</p>
</div>
{% endif %}
{% if let Some(fa) = fuehrungsassistent %}
<div class="cell">
<p><b>Führungsassistent geplant:</b> {{ fa }}</p>
<p><b>Führungsassistent geplant:</b> {{ fa.name }}</p>
</div>
{% endif %}
{% if posten.len() > 0 %}
<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>
{% endif %}
@ -242,7 +251,7 @@
<div class="cell is-col-span-2">
<b>Fahrzeuge:</b>
{% for v in vehicle %}
<span class="tag is-link"> {{ v.radio_call_name }} - {{ v.station }}</span>
<span class="tag is-link"> {{ v }}</span>
{% endfor %}
</div>
{% endif %}