From 366910ba0a2d21aa758182e2fd366fef5e1de08b Mon Sep 17 00:00:00 2001 From: Max Hohlfeld Date: Thu, 19 Dec 2024 23:10:44 +0100 Subject: [PATCH] feat: vehicle assignment for event --- ...166706b5adf4815de81481c9eb67d94b7ee0d.json | 41 +++++++++++++ ...8a964ebeff7249904f42b2d7b47371a9aea5.json} | 22 ++++--- ...7be5dc4e4ceacfc14b37472c39e65d9501be9.json | 32 ++++++++++ ...5699421fb09a26e505871b050e12ec6634c2.json} | 22 ++++--- ...8395e797fcdd19607cacfd1322037f3805c5.json} | 5 +- web/src/endpoints/events/get_plan.rs | 10 +++- web/src/endpoints/mod.rs | 3 + .../endpoints/vehicle_assignment/delete.rs | 59 +++++++++++++++++++ web/src/endpoints/vehicle_assignment/mod.rs | 13 ++++ .../endpoints/vehicle_assignment/post_new.rs | 25 ++++++-- web/src/models/vehicle_assignement.rs | 18 ++++++ web/src/utils/event_planning_template.rs | 21 ++++++- web/static/style.scss | 1 + web/templates/events/plan.html | 4 +- web/templates/events/plan_vehicles.html | 25 ++++++++ 15 files changed, 275 insertions(+), 26 deletions(-) create mode 100644 .sqlx/query-159c257e9e7a164d369de950940166706b5adf4815de81481c9eb67d94b7ee0d.json rename .sqlx/{query-efb827de66b93f6087ebfb49360abded5f7d5bef3b22db0fc3de466924b23782.json => query-bc6f85f2d5712c00966319960ceb8a964ebeff7249904f42b2d7b47371a9aea5.json} (73%) create mode 100644 .sqlx/query-c79fb8f25bf2e9f4299f690bebf7be5dc4e4ceacfc14b37472c39e65d9501be9.json rename .sqlx/{query-5c5c88811fd870d2f68b76fe71afd2ee1e72623c94be406e369af8a4a04591e0.json => query-df9d7eeb0c4c2d5b222896589dc65699421fb09a26e505871b050e12ec6634c2.json} (74%) rename .sqlx/{query-57d4a852f0845c761990cc1483bfc3e6f2e8b130427b7cae9e2376b45625195f.json => query-fae818e52e9e5cc9c38684afe5a08395e797fcdd19607cacfd1322037f3805c5.json} (55%) create mode 100644 web/src/endpoints/vehicle_assignment/delete.rs create mode 100644 web/templates/events/plan_vehicles.html diff --git a/.sqlx/query-159c257e9e7a164d369de950940166706b5adf4815de81481c9eb67d94b7ee0d.json b/.sqlx/query-159c257e9e7a164d369de950940166706b5adf4815de81481c9eb67d94b7ee0d.json new file mode 100644 index 00000000..9b181c7b --- /dev/null +++ b/.sqlx/query-159c257e9e7a164d369de950940166706b5adf4815de81481c9eb67d94b7ee0d.json @@ -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" +} diff --git a/.sqlx/query-efb827de66b93f6087ebfb49360abded5f7d5bef3b22db0fc3de466924b23782.json b/.sqlx/query-bc6f85f2d5712c00966319960ceb8a964ebeff7249904f42b2d7b47371a9aea5.json similarity index 73% rename from .sqlx/query-efb827de66b93f6087ebfb49360abded5f7d5bef3b22db0fc3de466924b23782.json rename to .sqlx/query-bc6f85f2d5712c00966319960ceb8a964ebeff7249904f42b2d7b47371a9aea5.json index 041cc24d..a2f3e2f4 100644 --- a/.sqlx/query-efb827de66b93f6087ebfb49360abded5f7d5bef3b22db0fc3de466924b23782.json +++ b/.sqlx/query-bc6f85f2d5712c00966319960ceb8a964ebeff7249904f42b2d7b47371a9aea5.json @@ -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" } diff --git a/.sqlx/query-c79fb8f25bf2e9f4299f690bebf7be5dc4e4ceacfc14b37472c39e65d9501be9.json b/.sqlx/query-c79fb8f25bf2e9f4299f690bebf7be5dc4e4ceacfc14b37472c39e65d9501be9.json new file mode 100644 index 00000000..d4b751e4 --- /dev/null +++ b/.sqlx/query-c79fb8f25bf2e9f4299f690bebf7be5dc4e4ceacfc14b37472c39e65d9501be9.json @@ -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" +} diff --git a/.sqlx/query-5c5c88811fd870d2f68b76fe71afd2ee1e72623c94be406e369af8a4a04591e0.json b/.sqlx/query-df9d7eeb0c4c2d5b222896589dc65699421fb09a26e505871b050e12ec6634c2.json similarity index 74% rename from .sqlx/query-5c5c88811fd870d2f68b76fe71afd2ee1e72623c94be406e369af8a4a04591e0.json rename to .sqlx/query-df9d7eeb0c4c2d5b222896589dc65699421fb09a26e505871b050e12ec6634c2.json index d85dff81..bcd00d5d 100644 --- a/.sqlx/query-5c5c88811fd870d2f68b76fe71afd2ee1e72623c94be406e369af8a4a04591e0.json +++ b/.sqlx/query-df9d7eeb0c4c2d5b222896589dc65699421fb09a26e505871b050e12ec6634c2.json @@ -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" } diff --git a/.sqlx/query-57d4a852f0845c761990cc1483bfc3e6f2e8b130427b7cae9e2376b45625195f.json b/.sqlx/query-fae818e52e9e5cc9c38684afe5a08395e797fcdd19607cacfd1322037f3805c5.json similarity index 55% rename from .sqlx/query-57d4a852f0845c761990cc1483bfc3e6f2e8b130427b7cae9e2376b45625195f.json rename to .sqlx/query-fae818e52e9e5cc9c38684afe5a08395e797fcdd19607cacfd1322037f3805c5.json index 614283af..0f417113 100644 --- a/.sqlx/query-57d4a852f0845c761990cc1483bfc3e6f2e8b130427b7cae9e2376b45625195f.json +++ b/.sqlx/query-fae818e52e9e5cc9c38684afe5a08395e797fcdd19607cacfd1322037f3805c5.json @@ -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" } diff --git a/web/src/endpoints/events/get_plan.rs b/web/src/endpoints/events/get_plan.rs index 4f8c02df..94347d1f 100644 --- a/web/src/endpoints/events/get_plan.rs +++ b/web/src/endpoints/events/get_plan.rs @@ -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, + vehicles_assigned: Vec, } #[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()?)) diff --git a/web/src/endpoints/mod.rs b/web/src/endpoints/mod.rs index 8dc0ccf3..83067704 100644 --- a/web/src/endpoints/mod.rs +++ b/web/src/endpoints/mod.rs @@ -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); } diff --git a/web/src/endpoints/vehicle_assignment/delete.rs b/web/src/endpoints/vehicle_assignment/delete.rs new file mode 100644 index 00000000..6cd50419 --- /dev/null +++ b/web/src/endpoints/vehicle_assignment/delete.rs @@ -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, + pool: web::Data, + query: web::Query, +) -> Result { + 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()?)) +} diff --git a/web/src/endpoints/vehicle_assignment/mod.rs b/web/src/endpoints/vehicle_assignment/mod.rs index fbafc930..4f42d35b 100644 --- a/web/src/endpoints/vehicle_assignment/mod.rs +++ b/web/src/endpoints/vehicle_assignment/mod.rs @@ -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, + vehicles_assigned: Vec, +} diff --git a/web/src/endpoints/vehicle_assignment/post_new.rs b/web/src/endpoints/vehicle_assignment/post_new.rs index a3bd88c0..2b540086 100644 --- a/web/src/endpoints/vehicle_assignment/post_new.rs +++ b/web/src/endpoints/vehicle_assignment/post_new.rs @@ -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, pool: web::Data, + form: web::Form, query: web::Query, ) -> Result { 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()?)) } diff --git a/web/src/models/vehicle_assignement.rs b/web/src/models/vehicle_assignement.rs index 87323660..bf550a12 100644 --- a/web/src/models/vehicle_assignement.rs +++ b/web/src/models/vehicle_assignement.rs @@ -31,6 +31,24 @@ impl VehicleAssignement { Ok(()) } + pub async fn read( + pool: &PgPool, + event_id: i32, + vehicle_id: i32, + ) -> Result> { + 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, diff --git a/web/src/utils/event_planning_template.rs b/web/src/utils/event_planning_template.rs index 39cb5ddf..0eeb39a6 100644 --- a/web/src/utils/event_planning_template.rs +++ b/web/src/utils/event_planning_template.rs @@ -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, Vec), 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, Vec) = + all_vehicles.into_iter().partition(|v| { + existing_vehicle_assignments_for_event + .iter() + .any(|va| va.vehicle_id == v.id) + }); + + Ok((vehicles_assigned, vehicles_available)) +} diff --git a/web/static/style.scss b/web/static/style.scss index 846eaea3..5f8a3254 100644 --- a/web/static/style.scss +++ b/web/static/style.scss @@ -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"; diff --git a/web/templates/events/plan.html b/web/templates/events/plan.html index cb6fdd0d..ba5a1041 100644 --- a/web/templates/events/plan.html +++ b/web/templates/events/plan.html @@ -56,7 +56,9 @@
Einteilung Fahrzeuge
- +
+ {% include "plan_vehicles.html" %} +
diff --git a/web/templates/events/plan_vehicles.html b/web/templates/events/plan_vehicles.html new file mode 100644 index 00000000..47bf8a06 --- /dev/null +++ b/web/templates/events/plan_vehicles.html @@ -0,0 +1,25 @@ +
+ {% for v in vehicles_assigned %} +
+
+ {{ v.radio_call_name }} - {{ v.station }} +
+
+ {% endfor %} +
+
+ +
+
+ +
+
+