From 1c6abc5882571d70130ef2a2236ecf40ec325f32 Mon Sep 17 00:00:00 2001 From: Max Hohlfeld Date: Wed, 20 Nov 2024 12:22:13 +0100 Subject: [PATCH] feat: vehicle management --- src/endpoints/availability/get_overview.rs | 2 +- src/endpoints/mod.rs | 8 +++ src/endpoints/vehicle/delete.rs | 27 ++++++++ src/endpoints/vehicle/get_edit.rs | 31 +++++++++ src/endpoints/vehicle/get_new.rs | 22 ++++++ src/endpoints/vehicle/get_overview.rs | 29 ++++++++ src/endpoints/vehicle/mod.rs | 24 +++++++ src/endpoints/vehicle/post_edit.rs | 39 +++++++++++ src/endpoints/vehicle/post_new.rs | 26 +++++++ src/models/location.rs | 2 +- src/models/mod.rs | 3 + src/models/vehicle.rs | 80 ++++++++++++++++++++++ src/models/vehicle_assignement.rs | 34 +++++++++ templates/nav.html | 37 +++++----- templates/vehicles/new_or_edit.html | 78 +++++++++++++++++++++ templates/vehicles/overview.html | 71 +++++++++++++++++++ 16 files changed, 490 insertions(+), 23 deletions(-) create mode 100644 src/endpoints/vehicle/delete.rs create mode 100644 src/endpoints/vehicle/get_edit.rs create mode 100644 src/endpoints/vehicle/get_new.rs create mode 100644 src/endpoints/vehicle/get_overview.rs create mode 100644 src/endpoints/vehicle/mod.rs create mode 100644 src/endpoints/vehicle/post_edit.rs create mode 100644 src/endpoints/vehicle/post_new.rs create mode 100644 src/models/vehicle_assignement.rs create mode 100644 templates/vehicles/new_or_edit.html create mode 100644 templates/vehicles/overview.html diff --git a/src/endpoints/availability/get_overview.rs b/src/endpoints/availability/get_overview.rs index cf366b18..47ade8ae 100644 --- a/src/endpoints/availability/get_overview.rs +++ b/src/endpoints/availability/get_overview.rs @@ -6,7 +6,7 @@ use chrono::{NaiveDate, Utc}; use serde::Deserialize; use sqlx::PgPool; -use crate::models::{Area, Availabillity, Event, Function, Role, User}; +use crate::models::{Area, Availabillity, Event, Role, User}; #[derive(Deserialize)] pub struct CalendarQuery { diff --git a/src/endpoints/mod.rs b/src/endpoints/mod.rs index 63c89f60..2801dc9f 100644 --- a/src/endpoints/mod.rs +++ b/src/endpoints/mod.rs @@ -10,6 +10,7 @@ mod export; mod location; mod user; mod imprint; +mod vehicle; #[derive(Deserialize)] pub struct IdPath { @@ -70,4 +71,11 @@ pub fn init(cfg: &mut ServiceConfig) { cfg.service(export::get_availability_data::get); cfg.service(imprint::get_imprint); + + cfg.service(vehicle::delete::delete); + cfg.service(vehicle::get_new::get); + cfg.service(vehicle::get_edit::get); + cfg.service(vehicle::get_overview::get); + cfg.service(vehicle::post_new::post); + cfg.service(vehicle::post_edit::post); } diff --git a/src/endpoints/vehicle/delete.rs b/src/endpoints/vehicle/delete.rs new file mode 100644 index 00000000..9f1471dd --- /dev/null +++ b/src/endpoints/vehicle/delete.rs @@ -0,0 +1,27 @@ +use actix_web::{web, HttpResponse, Responder}; +use sqlx::PgPool; + +use crate::{ + endpoints::IdPath, + models::{Role, User, Vehicle}, + utils::ApplicationError, +}; + +#[actix_web::delete("/vehicles/{id}")] +pub async fn delete( + user: web::ReqData, + pool: web::Data, + path: web::Path, +) -> Result { + if user.role != Role::Admin { + return Err(ApplicationError::Unauthorized); + } + + let Some(_) = Vehicle::read(pool.get_ref(), path.id).await? else { + return Ok(HttpResponse::NotFound().finish()); + }; + + Vehicle::delete(pool.get_ref(), path.id).await?; + + Ok(HttpResponse::Ok().finish()) +} diff --git a/src/endpoints/vehicle/get_edit.rs b/src/endpoints/vehicle/get_edit.rs new file mode 100644 index 00000000..392f6e61 --- /dev/null +++ b/src/endpoints/vehicle/get_edit.rs @@ -0,0 +1,31 @@ +use actix_web::{web, HttpResponse, Responder}; +use askama_actix::TemplateToResponse; +use sqlx::PgPool; + +use crate::{ + endpoints::{vehicle::VehicleNewOrEditTemplate, IdPath}, + models::{Role, User, Vehicle}, + utils::ApplicationError, +}; + +#[actix_web::get("/vehicles/{id}")] +pub async fn get( + user: web::ReqData, + pool: web::Data, + path: web::Path, +) -> Result { + if user.role != Role::Admin { + return Err(ApplicationError::Unauthorized); + } + + let Some(vehicle) = Vehicle::read(pool.get_ref(), path.id).await? else { + return Ok(HttpResponse::NotFound().finish()); + }; + + let template = VehicleNewOrEditTemplate { + user: user.into_inner(), + vehicle: Some(vehicle), + }; + + Ok(template.to_response()) +} diff --git a/src/endpoints/vehicle/get_new.rs b/src/endpoints/vehicle/get_new.rs new file mode 100644 index 00000000..3f6a1a44 --- /dev/null +++ b/src/endpoints/vehicle/get_new.rs @@ -0,0 +1,22 @@ +use actix_web::{web, Responder}; +use askama_actix::TemplateToResponse; + +use crate::{ + endpoints::vehicle::VehicleNewOrEditTemplate, + models::{Role, User}, + utils::ApplicationError, +}; + +#[actix_web::get("/vehicles/new")] +pub async fn get(user: web::ReqData) -> Result { + if user.role != Role::Admin { + return Err(ApplicationError::Unauthorized); + } + + let template = VehicleNewOrEditTemplate { + user: user.into_inner(), + vehicle: None, + }; + + Ok(template.to_response()) +} diff --git a/src/endpoints/vehicle/get_overview.rs b/src/endpoints/vehicle/get_overview.rs new file mode 100644 index 00000000..be2854eb --- /dev/null +++ b/src/endpoints/vehicle/get_overview.rs @@ -0,0 +1,29 @@ +use actix_web::{web, Responder}; +use askama::Template; +use askama_actix::TemplateToResponse; +use sqlx::PgPool; + +use crate::{models::{User, Vehicle, Role}, utils::ApplicationError}; + +#[derive(Template)] +#[template(path = "vehicles/overview.html")] +pub struct VehiclesOverviewTemplate { + user: User, + vehicles: Vec +} + +#[actix_web::get("/vehicles")] +pub async fn get(user: web::ReqData, pool: web::Data) -> Result { + if user.role != Role::Admin { + return Err(ApplicationError::Unauthorized); + } + + let vehicles = Vehicle::read_all(pool.get_ref()).await?; + + let template = VehiclesOverviewTemplate { + user: user.into_inner(), + vehicles + }; + + Ok(template.to_response()) +} diff --git a/src/endpoints/vehicle/mod.rs b/src/endpoints/vehicle/mod.rs new file mode 100644 index 00000000..3aceb5f4 --- /dev/null +++ b/src/endpoints/vehicle/mod.rs @@ -0,0 +1,24 @@ +use askama::Template; +use serde::Deserialize; + +use crate::models::{Role, User, Vehicle}; + +pub mod delete; +pub mod get_edit; +pub mod get_new; +pub mod get_overview; +pub mod post_edit; +pub mod post_new; + +#[derive(Template)] +#[template(path = "vehicles/new_or_edit.html")] +pub struct VehicleNewOrEditTemplate { + user: User, + vehicle: Option, +} + +#[derive(Deserialize)] +pub struct VehicleForm { + radio_call_name: String, + station: String, +} diff --git a/src/endpoints/vehicle/post_edit.rs b/src/endpoints/vehicle/post_edit.rs new file mode 100644 index 00000000..c1186bc3 --- /dev/null +++ b/src/endpoints/vehicle/post_edit.rs @@ -0,0 +1,39 @@ +use actix_web::{http::header::LOCATION, web, HttpResponse, Responder}; +use sqlx::PgPool; + +use crate::{ + endpoints::{vehicle::VehicleForm, IdPath}, + models::{Role, User, Vehicle}, + utils::ApplicationError, +}; + +#[actix_web::post("/vehicles/{id}")] +pub async fn post( + user: web::ReqData, + pool: web::Data, + path: web::Path, + form: web::Form, +) -> Result { + if user.role != Role::Admin { + return Err(ApplicationError::Unauthorized); + } + + let Some(vehicle) = Vehicle::read(pool.get_ref(), path.id).await? else { + return Ok(HttpResponse::NotFound().finish()); + }; + + if vehicle.radio_call_name != form.radio_call_name || vehicle.station != form.station { + Vehicle::update( + pool.get_ref(), + path.id, + &form.radio_call_name, + &form.station, + ) + .await?; + } + + Ok(HttpResponse::Found() + .insert_header((LOCATION, "/vehicles")) + .insert_header(("HX-LOCATION", "/vehicles")) + .finish()) +} diff --git a/src/endpoints/vehicle/post_new.rs b/src/endpoints/vehicle/post_new.rs new file mode 100644 index 00000000..4689dd7f --- /dev/null +++ b/src/endpoints/vehicle/post_new.rs @@ -0,0 +1,26 @@ +use actix_web::{http::header::LOCATION, web, HttpResponse, Responder}; +use sqlx::PgPool; + +use crate::{ + endpoints::vehicle::VehicleForm, + models::{Role, User, Vehicle}, + utils::ApplicationError, +}; + +#[actix_web::post("/vehicles/new")] +pub async fn post( + user: web::ReqData, + pool: web::Data, + form: web::Form, +) -> Result { + if user.role != Role::Admin { + return Err(ApplicationError::Unauthorized); + } + + Vehicle::create(pool.get_ref(), &form.radio_call_name, &form.station).await?; + + Ok(HttpResponse::Found() + .insert_header((LOCATION, "/vehicles")) + .insert_header(("HX-LOCATION", "/vehicles")) + .finish()) +} diff --git a/src/models/location.rs b/src/models/location.rs index ae3f2815..fcac73cb 100644 --- a/src/models/location.rs +++ b/src/models/location.rs @@ -108,7 +108,7 @@ impl Location { } pub async fn delete(pool: &PgPool, id: i32) -> super::Result<()> { - sqlx::query!("DELETE FROM location WHERE id = $1;", id) + query!("DELETE FROM location WHERE id = $1;", id) .execute(pool) .await?; diff --git a/src/models/mod.rs b/src/models/mod.rs index e1e7d13a..95b7a2d3 100644 --- a/src/models/mod.rs +++ b/src/models/mod.rs @@ -9,6 +9,7 @@ mod user; mod vehicle; mod password_reset; mod registration; +mod vehicle_assignement; pub use area::Area; pub use availabillity::Availabillity; @@ -20,5 +21,7 @@ pub use user::User; pub use assignement::Assignment; pub use password_reset::{PasswordReset, Token, NoneToken}; pub use registration::Registration; +pub use vehicle::Vehicle; +pub use vehicle_assignement::VehicleAssignement; type Result = std::result::Result; diff --git a/src/models/vehicle.rs b/src/models/vehicle.rs index e69de29b..dafd0581 100644 --- a/src/models/vehicle.rs +++ b/src/models/vehicle.rs @@ -0,0 +1,80 @@ +use sqlx::{query, PgPool}; + +use super::Result; + +pub struct Vehicle { + pub id: i32, + pub radio_call_name: String, + pub station: String, +} + +impl Vehicle { + pub async fn create(pool: &PgPool, radio_call_name: &str, station: &str) -> Result<()> { + query!( + "INSERT INTO vehicle (radioCallName, station) VALUES ($1, $2);", + radio_call_name, + station + ) + .execute(pool) + .await?; + + Ok(()) + } + + pub async fn read_all(pool: &PgPool) -> Result> { + let records = query!("SELECT * FROM vehicle;").fetch_all(pool).await?; + + let vehicles = records + .into_iter() + .map(|v| Vehicle { + id: v.id, + radio_call_name: v.radiocallname, + station: v.station, + }) + .collect(); + + Ok(vehicles) + } + + pub async fn read(pool: &PgPool, id: i32) -> Result> { + let record = query!("SELECT * FROM vehicle WHERE id = $1;", id) + .fetch_optional(pool) + .await?; + + let vehicle = record.and_then(|v| { + Some(Vehicle { + id: v.id, + radio_call_name: v.radiocallname, + station: v.station, + }) + }); + + Ok(vehicle) + } + + pub async fn update( + pool: &PgPool, + id: i32, + radio_call_name: &str, + station: &str, + ) -> Result<()> { + query!( + "UPDATE vehicle SET radiocallname = $1, station = $2 WHERE id = $3;", + radio_call_name, + station, + id + ) + .execute(pool) + .await?; + + Ok(()) + } + + pub async fn delete(pool: &PgPool, id: i32) -> Result<()> { + query!("DELETE FROM vehicle WHERE id = $1;", id) + .execute(pool) + .await?; + + Ok(()) + } +} diff --git a/src/models/vehicle_assignement.rs b/src/models/vehicle_assignement.rs new file mode 100644 index 00000000..f2db77dd --- /dev/null +++ b/src/models/vehicle_assignement.rs @@ -0,0 +1,34 @@ +use sqlx::{query, PgPool}; + +use super::Result; + +pub struct VehicleAssignement { + event_id: i32, + vehicle_id: i32, +} + +impl VehicleAssignement { + pub async fn create(pool: &PgPool, event_id: i32, vehicle_id: i32) -> Result<()> { + query!( + "INSERT INTO vehicleassignement (eventId, vehicleId) VALUES ($1, $2);", + event_id, + vehicle_id + ) + .execute(pool) + .await?; + + Ok(()) + } + + pub async fn delete(pool: &PgPool, event_id: i32, vehicle_id: i32) -> Result<()> { + query!( + "DELETE FROM vehicleassignement WHERE eventId = $1 AND vehicleId = $2;", + event_id, + vehicle_id + ) + .execute(pool) + .await?; + + Ok(()) + } +} diff --git a/templates/nav.html b/templates/nav.html index 3f125d94..2daf2628 100644 --- a/templates/nav.html +++ b/templates/nav.html @@ -21,9 +21,7 @@ Kalender - {% match user.role %} - {% when Role::Staff %} - {% when Role::AreaManager %} + {% if user.role == Role::AreaManager || user.role == Role::Admin %} - - Veranstaltungsorte - - - Nutzerverwaltung - - {% when Role::Admin %} + - - Veranstaltungsorte - - - Nutzerverwaltung - - - {% endmatch %} + {% endif %}