feat: vehicle management
This commit is contained in:
parent
5740248b96
commit
1c6abc5882
@ -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 {
|
||||
|
@ -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);
|
||||
}
|
||||
|
27
src/endpoints/vehicle/delete.rs
Normal file
27
src/endpoints/vehicle/delete.rs
Normal file
@ -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<User>,
|
||||
pool: web::Data<PgPool>,
|
||||
path: web::Path<IdPath>,
|
||||
) -> Result<impl Responder, ApplicationError> {
|
||||
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())
|
||||
}
|
31
src/endpoints/vehicle/get_edit.rs
Normal file
31
src/endpoints/vehicle/get_edit.rs
Normal file
@ -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<User>,
|
||||
pool: web::Data<PgPool>,
|
||||
path: web::Path<IdPath>,
|
||||
) -> Result<impl Responder, ApplicationError> {
|
||||
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())
|
||||
}
|
22
src/endpoints/vehicle/get_new.rs
Normal file
22
src/endpoints/vehicle/get_new.rs
Normal file
@ -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<User>) -> Result<impl Responder, ApplicationError> {
|
||||
if user.role != Role::Admin {
|
||||
return Err(ApplicationError::Unauthorized);
|
||||
}
|
||||
|
||||
let template = VehicleNewOrEditTemplate {
|
||||
user: user.into_inner(),
|
||||
vehicle: None,
|
||||
};
|
||||
|
||||
Ok(template.to_response())
|
||||
}
|
29
src/endpoints/vehicle/get_overview.rs
Normal file
29
src/endpoints/vehicle/get_overview.rs
Normal file
@ -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<Vehicle>
|
||||
}
|
||||
|
||||
#[actix_web::get("/vehicles")]
|
||||
pub async fn get(user: web::ReqData<User>, pool: web::Data<PgPool>) -> Result<impl Responder, ApplicationError> {
|
||||
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())
|
||||
}
|
24
src/endpoints/vehicle/mod.rs
Normal file
24
src/endpoints/vehicle/mod.rs
Normal file
@ -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<Vehicle>,
|
||||
}
|
||||
|
||||
#[derive(Deserialize)]
|
||||
pub struct VehicleForm {
|
||||
radio_call_name: String,
|
||||
station: String,
|
||||
}
|
39
src/endpoints/vehicle/post_edit.rs
Normal file
39
src/endpoints/vehicle/post_edit.rs
Normal file
@ -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<User>,
|
||||
pool: web::Data<PgPool>,
|
||||
path: web::Path<IdPath>,
|
||||
form: web::Form<VehicleForm>,
|
||||
) -> Result<impl Responder, ApplicationError> {
|
||||
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())
|
||||
}
|
26
src/endpoints/vehicle/post_new.rs
Normal file
26
src/endpoints/vehicle/post_new.rs
Normal file
@ -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<User>,
|
||||
pool: web::Data<PgPool>,
|
||||
form: web::Form<VehicleForm>,
|
||||
) -> Result<impl Responder, ApplicationError> {
|
||||
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())
|
||||
}
|
@ -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?;
|
||||
|
||||
|
@ -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<T> = std::result::Result<T, sqlx::Error>;
|
||||
|
@ -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<Vec<Vehicle>> {
|
||||
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<Option<Vehicle>> {
|
||||
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(())
|
||||
}
|
||||
}
|
34
src/models/vehicle_assignement.rs
Normal file
34
src/models/vehicle_assignement.rs
Normal file
@ -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(())
|
||||
}
|
||||
}
|
@ -21,9 +21,7 @@
|
||||
Kalender
|
||||
</a>
|
||||
|
||||
{% match user.role %}
|
||||
{% when Role::Staff %}
|
||||
{% when Role::AreaManager %}
|
||||
{% if user.role == Role::AreaManager || user.role == Role::Admin %}
|
||||
<div class="navbar-item has-dropdown is-hoverable">
|
||||
<div class="navbar-item">
|
||||
Export
|
||||
@ -35,32 +33,29 @@
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
<a href="/locations" class="navbar-item">
|
||||
Veranstaltungsorte
|
||||
</a>
|
||||
<a href="/users" class="navbar-item">
|
||||
Nutzerverwaltung
|
||||
</a>
|
||||
{% when Role::Admin %}
|
||||
|
||||
<div class="navbar-item has-dropdown is-hoverable">
|
||||
<div class="navbar-item">
|
||||
Export
|
||||
Verwaltung
|
||||
</div>
|
||||
|
||||
<div class="navbar-dropdown">
|
||||
<a href="/export/availability" class="navbar-item">
|
||||
Verfügbarkeiten
|
||||
<a href="/users" class="navbar-item">
|
||||
Nutzer
|
||||
</a>
|
||||
|
||||
<a href="/locations" class="navbar-item">
|
||||
Veranstaltungsorte
|
||||
</a>
|
||||
|
||||
{% if user.role == Role::Admin %}
|
||||
<a href="/vehicles" class="navbar-item">
|
||||
Fahrzeuge
|
||||
</a>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
<a href="/locations" class="navbar-item">
|
||||
Veranstaltungsorte
|
||||
</a>
|
||||
<a href="/users" class="navbar-item">
|
||||
Nutzerverwaltung
|
||||
</a>
|
||||
|
||||
{% endmatch %}
|
||||
{% endif %}
|
||||
</div>
|
||||
|
||||
<div class="navbar-end">
|
||||
|
78
templates/vehicles/new_or_edit.html
Normal file
78
templates/vehicles/new_or_edit.html
Normal file
@ -0,0 +1,78 @@
|
||||
{% extends "nav.html" %}
|
||||
|
||||
{% block content %}
|
||||
<section class="section">
|
||||
<div class="container">
|
||||
{% if vehicle.is_some() %}
|
||||
<form method="post" action="/vehicles/{{ vehicle.as_ref().unwrap().id }}">
|
||||
<h1 class="title">Fahrzeug '{{ vehicle.as_ref().unwrap().radio_call_name }}' bearbeiten</h1>
|
||||
{% else %}
|
||||
<form method="post" action="/vehicles/new">
|
||||
<h1 class="title">Neues Fahrzeug anlegen</h1>
|
||||
{% endif %}
|
||||
|
||||
<div class="field is-horizontal">
|
||||
<div class="field-label">
|
||||
<label class="label">Funkkenner</label>
|
||||
</div>
|
||||
<div class="field-body">
|
||||
<div class="field">
|
||||
<div class="control">
|
||||
<input class="input" type="text" name="radio_call_name" required placeholder="11.49.1" {% if
|
||||
vehicle.is_some() -%} value="{{ vehicle.as_ref().unwrap().radio_call_name }}" {% endif -%} />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="field is-horizontal">
|
||||
<div class="field-label">
|
||||
<label class="label">Wache</label>
|
||||
</div>
|
||||
<div class="field-body">
|
||||
<div class="field">
|
||||
<div class="control">
|
||||
<input class="input" type="text" name="station" required placeholder="FF Leipzig Ost" {% if
|
||||
vehicle.is_some() -%} value="{{ vehicle.as_ref().unwrap().station }}" {% endif -%} />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="field is-horizontal">
|
||||
<div class="field-label"></div>
|
||||
<div class="field-body">
|
||||
<div class="field is-grouped">
|
||||
<div class="control">
|
||||
<button class="button is-success">
|
||||
<svg class="icon">
|
||||
<use href="/static/feather-sprite.svg#check-circle" />
|
||||
</svg>
|
||||
<span>
|
||||
{% if vehicle.is_some() %}
|
||||
Speichern
|
||||
{% else %}
|
||||
Erstellen
|
||||
{% endif %}
|
||||
</span>
|
||||
</button>
|
||||
</div>
|
||||
<div class="control">
|
||||
<a class="button is-link is-light" hx-boost="true" href="/vehicles">
|
||||
<svg class="icon">
|
||||
<use href="/static/feather-sprite.svg#arrow-left" />
|
||||
</svg>
|
||||
<span>Zurück</span>
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</form>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<script>
|
||||
</script>
|
||||
{% endblock %}
|
71
templates/vehicles/overview.html
Normal file
71
templates/vehicles/overview.html
Normal file
@ -0,0 +1,71 @@
|
||||
{% extends "nav.html" %}
|
||||
|
||||
{% block content %}
|
||||
<section class="section">
|
||||
<div class="container">
|
||||
<div class="level">
|
||||
<div class="level-left">
|
||||
<h3 class="title is-3">
|
||||
Fahrzeuge
|
||||
</h3>
|
||||
</div>
|
||||
<div class="level-right">
|
||||
<a class="button is-link is-light" hx-boost="true" href="/vehicles/new">
|
||||
<svg class="icon">
|
||||
<use href="/static/feather-sprite.svg#plus-circle" />
|
||||
</svg>
|
||||
<span>Neues Fahrzeug anlegen</span>
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{% if vehicles.len() == 0 %}
|
||||
<div class="box">
|
||||
<h5 class="title is-5">keine Fahrzeuge vorhanden</h5>
|
||||
</div>
|
||||
{% else %}
|
||||
<div class="box">
|
||||
<table class="table is-fullwidth">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Funkkenner</th>
|
||||
<th>Wache</th>
|
||||
<th></th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{% for v in vehicles %}
|
||||
<tr>
|
||||
<td>
|
||||
{{ v.radio_call_name }}
|
||||
</td>
|
||||
<td>
|
||||
{{ v.station }}
|
||||
</td>
|
||||
<td>
|
||||
<div class="buttons is-right">
|
||||
<a class="button is-primary is-light" hx-boost="true" href="/vehicles/{{ v.id }}">
|
||||
<svg class="icon">
|
||||
<use href="/static/feather-sprite.svg#edit" />
|
||||
</svg>
|
||||
<span>Bearbeiten</span>
|
||||
</a>
|
||||
<button class="button is-danger is-light" hx-delete="/vehicles/{{ v.id }}" hx-target="closest tr"
|
||||
hx-swap="delete" hx-trigger="confirmed">
|
||||
<svg class="icon">
|
||||
<use href="/static/feather-sprite.svg#x-circle" />
|
||||
</svg>
|
||||
<span>Löschen</span>
|
||||
</button>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
</section>
|
||||
|
||||
{% endblock %}
|
Loading…
x
Reference in New Issue
Block a user