feat: vehicle assignment for event

This commit is contained in:
Max Hohlfeld 2024-12-19 23:10:44 +01:00
parent 954bba25d5
commit 366910ba0a
15 changed files with 275 additions and 26 deletions

View File

@ -0,0 +1,41 @@
{
"db_name": "PostgreSQL",
"query": "SELECT * FROM vehicleAssignement WHERE vehicleAssignement.eventId = $1 AND vehicleAssignement.vehicleId = $2;",
"describe": {
"columns": [
{
"ordinal": 0,
"name": "eventid",
"type_info": "Int4"
},
{
"ordinal": 1,
"name": "vehicleid",
"type_info": "Int4"
},
{
"ordinal": 2,
"name": "starttime",
"type_info": "Time"
},
{
"ordinal": 3,
"name": "endtime",
"type_info": "Time"
}
],
"parameters": {
"Left": [
"Int4",
"Int4"
]
},
"nullable": [
false,
false,
false,
false
]
},
"hash": "159c257e9e7a164d369de950940166706b5adf4815de81481c9eb67d94b7ee0d"
}

View File

@ -1,6 +1,6 @@
{
"db_name": "PostgreSQL",
"query": "\n SELECT\n event.id AS eventId,\n event.date,\n event.startTime,\n event.endTime,\n event.name,\n event.locationId,\n event.voluntaryWachhabender,\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 FROM event\n JOIN location ON event.locationId = location.id\n WHERE date = $1\n AND location.areaId = $2;\n ",
"query": "\n SELECT\n event.id AS eventId,\n event.date,\n event.startTime,\n event.endTime,\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 FROM event\n JOIN location ON event.locationId = location.id\n WHERE date = $1\n AND location.areaId = $2;\n ",
"describe": {
"columns": [
{
@ -40,36 +40,41 @@
},
{
"ordinal": 7,
"name": "voluntaryfuehrungsassistent",
"type_info": "Bool"
},
{
"ordinal": 8,
"name": "amountofposten",
"type_info": "Int2"
},
{
"ordinal": 8,
"ordinal": 9,
"name": "clothing",
"type_info": "Text"
},
{
"ordinal": 9,
"ordinal": 10,
"name": "canceled",
"type_info": "Bool"
},
{
"ordinal": 10,
"ordinal": 11,
"name": "note",
"type_info": "Text"
},
{
"ordinal": 11,
"ordinal": 12,
"name": "id",
"type_info": "Int4"
},
{
"ordinal": 12,
"ordinal": 13,
"name": "locationname",
"type_info": "Text"
},
{
"ordinal": 13,
"ordinal": 14,
"name": "locationareaid",
"type_info": "Int4"
}
@ -91,11 +96,12 @@
false,
false,
false,
false,
true,
false,
false,
false
]
},
"hash": "efb827de66b93f6087ebfb49360abded5f7d5bef3b22db0fc3de466924b23782"
"hash": "bc6f85f2d5712c00966319960ceb8a964ebeff7249904f42b2d7b47371a9aea5"
}

View File

@ -0,0 +1,32 @@
{
"db_name": "PostgreSQL",
"query": "\n SELECT *\n FROM vehicle\n\n ;",
"describe": {
"columns": [
{
"ordinal": 0,
"name": "id",
"type_info": "Int4"
},
{
"ordinal": 1,
"name": "radiocallname",
"type_info": "Text"
},
{
"ordinal": 2,
"name": "station",
"type_info": "Text"
}
],
"parameters": {
"Left": []
},
"nullable": [
false,
false,
false
]
},
"hash": "c79fb8f25bf2e9f4299f690bebf7be5dc4e4ceacfc14b37472c39e65d9501be9"
}

View File

@ -1,6 +1,6 @@
{
"db_name": "PostgreSQL",
"query": "\n SELECT\n event.id AS eventId,\n event.date,\n event.startTime,\n event.endTime,\n event.name,\n event.locationId,\n event.voluntaryWachhabender,\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 FROM event\n JOIN location ON event.locationId = location.id\n WHERE event.id = $1;\n ",
"query": "\n SELECT\n event.id AS eventId,\n event.date,\n event.startTime,\n event.endTime,\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 FROM event\n JOIN location ON event.locationId = location.id\n WHERE event.id = $1;\n ",
"describe": {
"columns": [
{
@ -40,36 +40,41 @@
},
{
"ordinal": 7,
"name": "voluntaryfuehrungsassistent",
"type_info": "Bool"
},
{
"ordinal": 8,
"name": "amountofposten",
"type_info": "Int2"
},
{
"ordinal": 8,
"ordinal": 9,
"name": "clothing",
"type_info": "Text"
},
{
"ordinal": 9,
"ordinal": 10,
"name": "canceled",
"type_info": "Bool"
},
{
"ordinal": 10,
"ordinal": 11,
"name": "note",
"type_info": "Text"
},
{
"ordinal": 11,
"ordinal": 12,
"name": "id",
"type_info": "Int4"
},
{
"ordinal": 12,
"ordinal": 13,
"name": "locationname",
"type_info": "Text"
},
{
"ordinal": 13,
"ordinal": 14,
"name": "locationareaid",
"type_info": "Int4"
}
@ -90,11 +95,12 @@
false,
false,
false,
false,
true,
false,
false,
false
]
},
"hash": "5c5c88811fd870d2f68b76fe71afd2ee1e72623c94be406e369af8a4a04591e0"
"hash": "df9d7eeb0c4c2d5b222896589dc65699421fb09a26e505871b050e12ec6634c2"
}

View File

@ -1,6 +1,6 @@
{
"db_name": "PostgreSQL",
"query": "\n INSERT INTO event (date, startTime, endTime, name, locationId, voluntaryWachhabender, amountOfPosten, clothing, note)\n VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9);\n ",
"query": "\n INSERT INTO event (date, startTime, endTime, name, locationId, voluntaryWachhabender, voluntaryFuehrungsassistent, amountOfPosten, clothing, note)\n VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10);\n ",
"describe": {
"columns": [],
"parameters": {
@ -11,6 +11,7 @@
"Text",
"Int4",
"Bool",
"Bool",
"Int2",
"Text",
"Text"
@ -18,5 +19,5 @@
},
"nullable": []
},
"hash": "57d4a852f0845c761990cc1483bfc3e6f2e8b130427b7cae9e2376b45625195f"
"hash": "fae818e52e9e5cc9c38684afe5a08395e797fcdd19607cacfd1322037f3805c5"
}

View File

@ -5,10 +5,11 @@ use sqlx::PgPool;
use crate::{
endpoints::IdPath,
filters,
models::{Availabillity, AvailabillityAssignmentState, Event, Function, Role, User},
models::{Availabillity, AvailabillityAssignmentState, Event, Function, Role, User, Vehicle},
utils::{
event_planning_template::{
generate_availabillity_assignment_list, generate_status_whether_staff_is_required,
generate_vehicles_assigned_and_available,
},
ApplicationError,
},
@ -23,6 +24,8 @@ pub struct PlanEventTemplate {
further_posten_required: bool,
further_fuehrungsassistent_required: bool,
further_wachhabender_required: bool,
vehicles_available: Vec<Vehicle>,
vehicles_assigned: Vec<Vehicle>,
}
#[actix_web::get("/events/{id}/plan")]
@ -51,6 +54,9 @@ pub async fn get(
further_wachhabender_required,
) = generate_status_whether_staff_is_required(pool.get_ref(), &event).await?;
let (vehicles_assigned, vehicles_available) =
generate_vehicles_assigned_and_available(pool.get_ref(), &event).await?;
let template = PlanEventTemplate {
user: user.into_inner(),
event,
@ -58,6 +64,8 @@ pub async fn get(
further_posten_required,
further_fuehrungsassistent_required,
further_wachhabender_required,
vehicles_assigned,
vehicles_available,
};
Ok(HttpResponse::Ok().body(template.render()?))

View File

@ -80,4 +80,7 @@ pub fn init(cfg: &mut ServiceConfig) {
cfg.service(vehicle::get_overview::get);
cfg.service(vehicle::post_new::post);
cfg.service(vehicle::post_edit::post);
cfg.service(vehicle_assignment::post_new::post);
cfg.service(vehicle_assignment::delete::delete);
}

View File

@ -0,0 +1,59 @@
use actix_web::{web, HttpResponse, Responder};
use rinja::Template;
use serde::Deserialize;
use sqlx::PgPool;
use crate::{
endpoints::vehicle_assignment::PlanVehiclesPartialTemplate,
models::{Event, Role, User, VehicleAssignement},
utils::{event_planning_template::generate_vehicles_assigned_and_available, ApplicationError},
};
#[derive(Deserialize)]
struct VehicleAssignmentDeleteQuery {
vehicle: i32,
event: i32,
}
#[actix_web::delete("/vehicleassignments/delete")]
pub async fn delete(
user: web::ReqData<User>,
pool: web::Data<PgPool>,
query: web::Query<VehicleAssignmentDeleteQuery>,
) -> Result<impl Responder, ApplicationError> {
let Some(event) = Event::read_by_id_including_location(pool.get_ref(), query.event).await?
else {
return Ok(HttpResponse::NotFound().finish());
};
let user_is_admin_or_area_manager_of_event_area = user.role == Role::Admin
|| (user.role == Role::AreaManager
&& user.area_id == event.location.as_ref().unwrap().area_id);
if !user_is_admin_or_area_manager_of_event_area {
return Err(ApplicationError::Unauthorized);
}
let Some(vehicle_assignment) =
VehicleAssignement::read(pool.get_ref(), event.id, query.vehicle).await?
else {
return Ok(HttpResponse::NotFound().finish());
};
VehicleAssignement::delete(
pool.get_ref(),
vehicle_assignment.event_id,
vehicle_assignment.vehicle_id,
)
.await?;
let (vehicles_assigned, vehicles_available) =
generate_vehicles_assigned_and_available(pool.get_ref(), &event).await?;
let template = PlanVehiclesPartialTemplate {
event,
vehicles_assigned,
vehicles_available,
};
Ok(HttpResponse::Ok().body(template.render()?))
}

View File

@ -1 +1,14 @@
use rinja::Template;
use crate::models::{Event, Vehicle};
pub mod post_new;
pub mod delete;
#[derive(Template)]
#[template(path = "events/plan_vehicles.html")]
pub struct PlanVehiclesPartialTemplate {
event: Event,
vehicles_available: Vec<Vehicle>,
vehicles_assigned: Vec<Vehicle>,
}

View File

@ -4,11 +4,12 @@ use serde::Deserialize;
use sqlx::PgPool;
use crate::{
endpoints::assignment::PlanEventPersonalTablePartialTemplate,
endpoints::{assignment::PlanEventPersonalTablePartialTemplate, vehicle_assignment::PlanVehiclesPartialTemplate},
models::{Assignment, Availabillity, Event, Function, Role, User, Vehicle, VehicleAssignement},
utils::{
event_planning_template::{
generate_availabillity_assignment_list, generate_status_whether_staff_is_required,
generate_vehicles_assigned_and_available,
},
ApplicationError,
},
@ -16,14 +17,19 @@ use crate::{
#[derive(Deserialize)]
pub struct VehicleAssignmentQuery {
vehicle: i32,
event: i32,
}
#[derive(Deserialize)]
pub struct VehicleAssignmentForm {
vehicle: i32,
}
#[actix_web::post("/vehicleassignments/new")]
pub async fn post(
user: web::ReqData<User>,
pool: web::Data<PgPool>,
form: web::Form<VehicleAssignmentForm>,
query: web::Query<VehicleAssignmentQuery>,
) -> Result<impl Responder, ApplicationError> {
let Some(event) = Event::read_by_id_including_location(pool.get_ref(), query.event).await?
@ -39,7 +45,7 @@ pub async fn post(
return Err(ApplicationError::Unauthorized);
}
let Some(vehicle) = Vehicle::read(pool.get_ref(), query.vehicle).await? else {
let Some(vehicle) = Vehicle::read(pool.get_ref(), form.vehicle).await? else {
return Ok(HttpResponse::NotFound().finish());
};
@ -55,7 +61,8 @@ pub async fn post(
.any(|a| has_start_time_during_event(a) || has_end_time_during_event(a));
if availability_already_assigned {
return Ok(HttpResponse::BadRequest().body("Vehicle already assigned to a timely conflicting event."));
return Ok(HttpResponse::BadRequest()
.body("Vehicle already assigned to a timely conflicting event."));
}
VehicleAssignement::create(
@ -67,5 +74,13 @@ pub async fn post(
)
.await?;
return Ok(HttpResponse::NotFound().finish());
let (vehicles_assigned, vehicles_available) =
generate_vehicles_assigned_and_available(pool.get_ref(), &event).await?;
let template = PlanVehiclesPartialTemplate {
event,
vehicles_assigned,
vehicles_available,
};
Ok(HttpResponse::Ok().body(template.render()?))
}

View File

@ -31,6 +31,24 @@ impl VehicleAssignement {
Ok(())
}
pub async fn read(
pool: &PgPool,
event_id: i32,
vehicle_id: i32,
) -> Result<Option<VehicleAssignement>> {
let record = query!("SELECT * FROM vehicleAssignement WHERE vehicleAssignement.eventId = $1 AND vehicleAssignement.vehicleId = $2;", event_id, vehicle_id).fetch_optional(pool)
.await?;
let vehicle_assignment = record.and_then(|r| Some(VehicleAssignement {
event_id: r.eventid,
vehicle_id: r.vehicleid,
start_time: r.starttime,
end_time: r.endtime,
}));
Ok(vehicle_assignment)
}
pub async fn read_all_by_event(
pool: &PgPool,
event_id: i32,

View File

@ -1,6 +1,8 @@
use sqlx::PgPool;
use crate::models::{Assignment, Availabillity, AvailabillityAssignmentState, Event, Function};
use crate::models::{
Assignment, Availabillity, AvailabillityAssignmentState, Event, Function, Vehicle, VehicleAssignement,
};
use super::ApplicationError;
@ -90,3 +92,20 @@ pub async fn generate_status_whether_staff_is_required(
further_wachhabender_required,
))
}
pub async fn generate_vehicles_assigned_and_available(
pool: &PgPool,
event: &Event,
) -> Result<(Vec<Vehicle>, Vec<Vehicle>), ApplicationError> {
let all_vehicles = Vehicle::read_all(pool).await?;
let existing_vehicle_assignments_for_event =
VehicleAssignement::read_all_by_event(pool, event.id).await?;
let (vehicles_assigned, vehicles_available): (Vec<Vehicle>, Vec<Vehicle>) =
all_vehicles.into_iter().partition(|v| {
existing_vehicle_assignments_for_event
.iter()
.any(|va| va.vehicle_id == v.id)
});
Ok((vehicles_assigned, vehicles_available))
}

View File

@ -26,6 +26,7 @@ $primary: $crimson,
@forward "bulma/sass/elements/tag";
@forward "bulma/sass/elements/table";
@forward "bulma/sass/elements/title";
@forward "bulma/sass/elements/delete";
@forward "bulma/sass/form";

View File

@ -56,7 +56,9 @@
<div class="box">
<h5 class="title is-5">Einteilung Fahrzeuge</h5>
<div id="vehicle-plan">
{% include "plan_vehicles.html" %}
</div>
</div>
</div>
</section>

View File

@ -0,0 +1,25 @@
<div class="field is-grouped is-grouped-multiline">
{% for v in vehicles_assigned %}
<div class="control">
<div class="tags has-addons">
<span class="tag is-link"> {{ v.radio_call_name }} - {{ v.station }}</span>
<button class="tag is-delete" hx-delete="/vehicleassignments/delete?event={{ event.id }}&vehicle={{ v.id }}"
hx-target="#vehicle-plan" />
</div>
</div>
{% endfor %}
</div>
<div class="field">
<label class="label">Fahrzeug hinzufügen</label>
<div class="control">
<div class="select">
<select name="vehicle" hx-post="/vehicleassignments/new?event={{ event.id }}" hx-include="this"
hx-target="#vehicle-plan">
<option selected></option>
{% for v in vehicles_available %}
<option value="{{ v.id }}">{{ v.radio_call_name }} - {{ v.station }}</option>
{% endfor %}
</select>
</div>
</div>
</div>