feat: WIP add clothing table

This commit is contained in:
Max Hohlfeld 2025-05-15 22:58:32 +02:00
parent c9b075216a
commit 5b5e312152
18 changed files with 249 additions and 18 deletions

View File

@ -0,0 +1,11 @@
CREATE TABLE clothing
(
id SERIAL PRIMARY KEY,
name TEXT NOT NULL
);
INSERT INTO clothing (name) VALUES ('Tuchuniform');
ALTER TABLE event
ALTER COLUMN clothing SET DATA TYPE INTEGER USING 1,
ADD FOREIGN KEY (clothing) REFERENCES clothing (id) ON DELETE CASCADE;

View File

@ -0,0 +1,86 @@
use actix_web::{web, Responder};
use askama::Template;
use sqlx::PgPool;
use crate::{
models::{Clothing, Role, User},
utils::{ApplicationError, TemplateResponse},
};
#[derive(Template)]
#[cfg_attr(not(test), template(path = "clothing/overview.html"))]
#[cfg_attr(
test,
template(path = "clothing/overview.html", block = "content"),
allow(dead_code)
)]
pub struct ClothingOverviewTemplate {
user: User,
clothings: Vec<Clothing>,
}
#[actix_web::get("/clothing")]
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 clothings = Clothing::read_all(pool.get_ref()).await?;
let template = ClothingOverviewTemplate {
user: user.into_inner(),
clothings,
};
Ok(template.to_response()?)
}
#[cfg(test)]
mod tests {
use crate::{
models::{Clothing, Role},
utils::test_helper::{
assert_snapshot, read_body, test_get, DbTestContext, RequestConfig, StatusCode,
},
};
use brass_macros::db_test;
#[db_test]
async fn user_cant_view_overview(context: &DbTestContext) {
let app = context.app().await;
let config = RequestConfig::new("/clothing");
let response = test_get(&context.db_pool, &app, &config).await;
assert_eq!(StatusCode::UNAUTHORIZED, response.status());
}
#[db_test]
async fn area_manager_cant_view_overview(context: &DbTestContext) {
let app = context.app().await;
let config = RequestConfig::new("/clothing").with_role(Role::AreaManager);
let response = test_get(&context.db_pool, &app, &config).await;
assert_eq!(StatusCode::UNAUTHORIZED, response.status());
}
#[db_test]
async fn produces_template_fine_when_user_is_admin(context: &DbTestContext) {
let app = context.app().await;
Clothing::create(&context.db_pool, "Schutzkleidung Form 1")
.await
.unwrap();
let config = RequestConfig::new("/clothing").with_role(Role::Admin);
let response = test_get(&context.db_pool, &app, &config).await;
assert_eq!(StatusCode::OK, response.status());
let body = read_body(response).await;
assert_snapshot!(body);
}
}

View File

@ -0,0 +1 @@
pub mod get_overview;

View File

@ -59,7 +59,7 @@ pub async fn get(
voluntary_wachhabender: event.voluntary_wachhabender,
voluntary_fuehrungsassistent: event.voluntary_fuehrungsassistent,
amount_of_posten: Some(event.amount_of_posten),
clothing: Some(event.clothing),
clothing: Some(event.clothing.id),
canceled: event.canceled,
note: event.note,
};
@ -107,7 +107,7 @@ async fn produces_template(context: &DbTestContext) {
voluntary_fuehrungsassistent: false,
voluntary_wachhabender: false,
amount_of_posten: 2,
clothing: "Tuchuniform".to_string(),
clothing: 1,
note: None,
};

View File

@ -30,7 +30,7 @@ pub struct NewOrEditEventTemplate {
voluntary_wachhabender: bool,
voluntary_fuehrungsassistent: bool,
amount_of_posten: Option<i16>,
clothing: Option<String>,
clothing: Option<i32>,
canceled: bool,
note: Option<String>,
amount_of_planned_posten: usize,
@ -49,7 +49,7 @@ pub struct NewOrEditEventForm {
voluntarywachhabender: bool,
voluntaryfuehrungsassistent: bool,
amount: i16,
clothing: String,
clothing: i32,
note: Option<String>,
}

View File

@ -45,7 +45,7 @@ pub async fn post(
let changeset = EventChangeset {
amount_of_posten: form.amount,
clothing: form.clothing.clone(),
clothing: form.clothing,
location_id: form.location,
time: (form.date.and_time(form.start), form.end),
name: form.name.clone(),

View File

@ -28,7 +28,7 @@ pub async fn post(
let changeset = EventChangeset {
amount_of_posten: form.amount,
clothing: form.clothing.clone(),
clothing: form.clothing,
location_id: form.location,
time: (form.date.and_time(form.start), form.end),
name: form.name.clone(),

View File

@ -5,11 +5,12 @@ use serde::Deserialize;
mod area;
mod assignment;
mod availability;
mod clothing;
mod events;
mod export;
mod imprint;
mod location;
pub mod user;
pub mod user; // TODO: why pub?
mod vehicle;
mod vehicle_assignment;

View File

@ -0,0 +1,62 @@
use sqlx::{query, PgPool};
use super::Result;
#[derive(Debug, Clone)]
pub struct Clothing {
pub id: i32,
pub name: String,
}
impl Clothing {
pub async fn create(pool: &PgPool, name: &str) -> Result<()> {
query!("INSERT INTO clothing (name) VALUES ($1);", name)
.execute(pool)
.await?;
Ok(())
}
pub async fn read_all(pool: &PgPool) -> Result<Vec<Clothing>> {
let records = query!("SELECT * FROM clothing;").fetch_all(pool).await?;
let clothing_options = records
.into_iter()
.map(|v| Clothing {
id: v.id,
name: v.name,
})
.collect();
Ok(clothing_options)
}
pub async fn read(pool: &PgPool, id: i32) -> Result<Option<Clothing>> {
let record = query!("SELECT * FROM clothing WHERE id = $1;", id)
.fetch_optional(pool)
.await?;
let vehicle = record.map(|v| Clothing {
id: v.id,
name: v.name,
});
Ok(vehicle)
}
pub async fn update(pool: &PgPool, id: i32, name: &str) -> Result<()> {
query!("UPDATE clothing SET name = $1 WHERE id = $2;", name, id)
.execute(pool)
.await?;
Ok(())
}
pub async fn delete(pool: &PgPool, id: i32) -> Result<()> {
query!("DELETE FROM clothing WHERE id = $1;", id)
.execute(pool)
.await?;
Ok(())
}
}

View File

@ -1,7 +1,7 @@
use chrono::{NaiveDate, NaiveDateTime};
use sqlx::{query, PgPool};
use super::{event_changeset::EventChangeset, Location, Result};
use super::{event_changeset::EventChangeset, Clothing, Location, Result};
#[derive(Clone, Debug)]
pub struct Event {
@ -14,7 +14,7 @@ pub struct Event {
pub voluntary_wachhabender: bool,
pub voluntary_fuehrungsassistent: bool,
pub amount_of_posten: i16,
pub clothing: String,
pub clothing: Clothing,
pub canceled: bool,
pub note: Option<String>,
}
@ -51,9 +51,12 @@ impl Event {
event.note,
location.id,
location.name AS locationName,
location.areaId AS locationAreaId
location.areaId AS locationAreaId,
clothing.id AS clothingId,
clothing.name AS clothingName
FROM event
JOIN location ON event.locationId = location.id
JOIN clothing ON event.clothing = clothing.id
WHERE starttimestamp::date = $1
AND location.areaId = $2;
"#,
@ -80,7 +83,10 @@ impl Event {
voluntary_wachhabender: record.voluntarywachhabender,
voluntary_fuehrungsassistent: record.voluntaryfuehrungsassistent,
amount_of_posten: record.amountofposten,
clothing: record.clothing.to_string(),
clothing: Clothing {
id: record.clothingid,
name: record.clothingname,
},
canceled: record.canceled,
note: record.note,
})
@ -106,9 +112,12 @@ impl Event {
event.note,
location.id,
location.name AS locationName,
location.areaId AS locationAreaId
location.areaId AS locationAreaId,
clothing.id AS clothingId,
clothing.name AS clothingName
FROM event
JOIN location ON event.locationId = location.id
JOIN clothing ON event.clothing = clothing.id
WHERE event.id = $1;
"#,
id
@ -131,7 +140,10 @@ impl Event {
voluntary_wachhabender: record.voluntarywachhabender,
voluntary_fuehrungsassistent: record.voluntaryfuehrungsassistent,
amount_of_posten: record.amountofposten,
clothing: record.clothing.to_string(),
clothing: Clothing {
id: record.clothingid,
name: record.clothingname,
},
canceled: record.canceled,
note: record.note,
});

View File

@ -25,7 +25,7 @@ pub struct EventChangeset {
pub voluntary_fuehrungsassistent: bool,
#[garde(range(min = ctx.as_ref().map(|c: &EventContext| c.amount_of_assigned_posten).unwrap_or(0), max = 100))]
pub amount_of_posten: i16,
pub clothing: String,
pub clothing: i32,
pub note: Option<String>,
}
@ -39,7 +39,7 @@ impl EventChangeset {
voluntary_wachhabender: true,
voluntary_fuehrungsassistent: true,
amount_of_posten: 5,
clothing: "Tuchuniform".to_string(),
clothing: 1,
note: None,
};

View File

@ -4,6 +4,7 @@ mod assignment_changeset;
mod availability;
mod availability_assignment_state;
mod availability_changeset;
mod clothing;
mod event;
mod event_changeset;
mod function;
@ -25,6 +26,7 @@ pub use availability_assignment_state::AvailabilityAssignmentState;
pub use availability_changeset::{
find_free_date_time_slots, AvailabilityChangeset, AvailabilityContext,
};
pub use clothing::Clothing;
pub use event::Event;
pub use event_changeset::{EventChangeset, EventContext};
pub use function::Function;

View File

@ -0,0 +1,15 @@
<li>
<input class="input" type="text" value="{{ c.name }}" />
<div class="buttons">
<button class="button">
<svg class="icon">
<use href="/static/feather-sprite.svg#check-circle" />
</svg>
</button>
<button class="button">
<svg class="icon">
<use href="/static/feather-sprite.svg#x-circle" />
</svg>
</button>
</div>
</li>

View File

@ -0,0 +1,15 @@
<li>
{{ c.name }}"
<div class="buttons">
<button class="button">
<svg class="icon">
<use href="/static/feather-sprite.svg#edit" />
</svg>
</button>
<button class="button">
<svg class="icon">
<use href="/static/feather-sprite.svg#x-circle" />
</svg>
</button>
</div>
</li>

View File

@ -0,0 +1,20 @@
{% extends "nav.html" %}
{% block content %}
<section class="section">
<div class="container">
<h3 class="title is-3">
Anzugsordnungen
</h3>
<p class="content">zur Auswahl bei der Erstellung von Events</p>
<div class="box">
<ul>
{% for c in clothings %}
{% include "clothing_entry_read.html" %}
{% endfor %}
</ul>
</div>
</section>
{% endblock %}

View File

@ -39,7 +39,7 @@
</div>
<div class="cell is-col-span-2">
<p><b>Anzugsordnung:</b> {{ event.clothing }}</p>
<p><b>Anzugsordnung:</b> {{ event.clothing.name }}</p>
</div>
<div class="cell is-col-span-2">

View File

@ -127,7 +127,7 @@
</div>
<div class="cell is-col-span-2">
<p><b>Anzugsordnung:</b> {{ event.clothing }}</p>
<p><b>Anzugsordnung:</b> {{ event.clothing.name }}</p>
</div>
{% if let Some(note) = event.note %}

View File

@ -8,7 +8,7 @@
</a>
<a role="button" class="navbar-burger" aria-label="menu" aria-expanded="false"
hx-on:click="[this, document.getElementById('navMenu')].forEach(e => e.classList.toggle('is-active'));">
hx-on:click="[this, document.getElementById('navMenu')].forEach(e => e.classList.toggle('is-active'));">
<span aria-hidden="true"></span>
<span aria-hidden="true"></span>
<span aria-hidden="true"></span>
@ -51,6 +51,12 @@
<a href="/vehicles" class="navbar-item">
Fahrzeuge
</a>
{% if user.role == Role::Admin %}
<a href="/clothing" class="navbar-item">
Anzugsordnung
</a>
{% endif %}
</div>
</div>
{% endif %}