feat: WIP clothing edit and delete

This commit is contained in:
Max Hohlfeld 2025-05-20 20:55:36 +02:00
parent 5b5e312152
commit 8d4a981055
12 changed files with 266 additions and 29 deletions

View File

@ -0,0 +1,27 @@
use actix_web::{web, HttpResponse, Responder};
use sqlx::PgPool;
use crate::{
endpoints::IdPath,
models::{Clothing, Role, User},
utils::ApplicationError,
};
#[actix_web::delete("/clothing/{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(clothing) = Clothing::read(pool.get_ref(), path.id).await? else {
return Ok(HttpResponse::NotFound().finish());
};
Clothing::delete(pool.get_ref(), clothing.id).await?;
Ok(HttpResponse::Ok().finish())
}

View File

@ -0,0 +1,30 @@
use actix_web::{web, HttpResponse, Responder};
use sqlx::PgPool;
use crate::{
endpoints::{clothing::EditClothingPartialTemplate, IdPath},
models::{Clothing, Role, User},
utils::{ApplicationError, TemplateResponse},
};
#[actix_web::get("/clothing/edit/{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(clothing) = Clothing::read(pool.get_ref(), path.id).await? else {
return Ok(HttpResponse::NotFound().finish());
};
let template = EditClothingPartialTemplate {
id: Some(clothing.id),
name: Some(clothing.name),
};
Ok(template.to_response()?)
}

View File

@ -0,0 +1,21 @@
use actix_web::{web, Responder};
use crate::{
endpoints::clothing::EditClothingPartialTemplate,
models::{Role, User},
utils::{ApplicationError, TemplateResponse},
};
#[actix_web::get("/clothing/new")]
pub async fn get(user: web::ReqData<User>) -> Result<impl Responder, ApplicationError> {
if user.role != Role::Admin {
return Err(ApplicationError::Unauthorized);
}
let template = EditClothingPartialTemplate {
id: None,
name: None,
};
Ok(template.to_response()?)
}

View File

@ -0,0 +1,27 @@
use actix_web::{web, HttpResponse, Responder};
use sqlx::PgPool;
use crate::{
endpoints::{clothing::ReadClothingPartialTemplate, IdPath},
models::{Clothing, Role, User},
utils::{ApplicationError, TemplateResponse},
};
#[actix_web::get("/clothing/{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(clothing) = Clothing::read(pool.get_ref(), path.id).await? else {
return Ok(HttpResponse::NotFound().finish());
};
let template = ReadClothingPartialTemplate { c: clothing };
Ok(template.to_response()?)
}

View File

@ -1 +1,31 @@
use askama::Template;
use serde::Deserialize;
use crate::models::Clothing;
use crate::filters;
pub mod delete;
pub mod get_edit;
pub mod get_new;
pub mod get_overview;
pub mod get_read;
pub mod post_edit;
pub mod post_new;
#[derive(Template)]
#[template(path = "clothing/clothing_entry_edit.html")]
struct EditClothingPartialTemplate {
id: Option<i32>,
name: Option<String>,
}
#[derive(Template)]
#[template(path = "clothing/clothing_entry_read.html")]
struct ReadClothingPartialTemplate {
c: Clothing,
}
#[derive(Deserialize)]
struct NewOrEditClothingForm {
name: String,
}

View File

@ -0,0 +1,35 @@
use actix_web::{web, HttpResponse, Responder};
use sqlx::PgPool;
use crate::{
endpoints::{
clothing::{NewOrEditClothingForm, ReadClothingPartialTemplate},
IdPath,
},
models::{Clothing, Role, User},
utils::{ApplicationError, TemplateResponse},
};
#[actix_web::post("/clothing/{id}")]
pub async fn post(
user: web::ReqData<User>,
pool: web::Data<PgPool>,
path: web::Path<IdPath>,
form: web::Form<NewOrEditClothingForm>,
) -> Result<impl Responder, ApplicationError> {
if user.role == Role::Admin {
return Err(ApplicationError::Unauthorized);
}
let Some(mut clothing) = Clothing::read(pool.get_ref(), path.id).await? else {
return Ok(HttpResponse::NotFound().finish());
};
clothing.name = form.name.to_string();
Clothing::update(pool.get_ref(), clothing.id, &clothing.name).await?;
let template = ReadClothingPartialTemplate { c: clothing };
Ok(template.to_response()?)
}

View File

@ -0,0 +1,25 @@
use actix_web::{web, Responder};
use sqlx::PgPool;
use crate::{
endpoints::clothing::{NewOrEditClothingForm, ReadClothingPartialTemplate},
models::{Clothing, Role, User},
utils::{ApplicationError, TemplateResponse},
};
#[actix_web::post("/clothing")]
pub async fn post(
user: web::ReqData<User>,
pool: web::Data<PgPool>,
form: web::Form<NewOrEditClothingForm>,
) -> Result<impl Responder, ApplicationError> {
if user.role == Role::Admin {
return Err(ApplicationError::Unauthorized);
}
let clothing = Clothing::create(pool.get_ref(), &form.name).await?;
let template = ReadClothingPartialTemplate { c: clothing };
Ok(template.to_response()?)
}

View File

@ -93,4 +93,12 @@ pub fn init(cfg: &mut ServiceConfig) {
cfg.service(vehicle_assignment::post_new::post);
cfg.service(vehicle_assignment::delete::delete);
cfg.service(clothing::get_overview::get);
cfg.service(clothing::get_edit::get);
cfg.service(clothing::get_new::get);
cfg.service(clothing::get_read::get);
cfg.service(clothing::delete::delete);
cfg.service(clothing::post_new::post);
cfg.service(clothing::post_edit::post);
}

View File

@ -9,12 +9,17 @@ pub struct Clothing {
}
impl Clothing {
pub async fn create(pool: &PgPool, name: &str) -> Result<()> {
query!("INSERT INTO clothing (name) VALUES ($1);", name)
.execute(pool)
pub async fn create(pool: &PgPool, name: &str) -> Result<Clothing> {
let r = query!("INSERT INTO clothing (name) VALUES ($1) RETURNING id;", name)
.fetch_one(pool)
.await?;
Ok(())
let created_clothing = Clothing {
id: r.id,
name: name.to_string()
};
Ok(created_clothing)
}
pub async fn read_all(pool: &PgPool) -> Result<Vec<Clothing>> {

View File

@ -1,15 +1,33 @@
<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>
<form method="post" action="/clothing/{% if let Some(id) = id %}{{ id }}{% else %}new{% endif %}">
<div class="level">
<div class="level-left">
<div class="level-item">
<input class="input" type="text" {{ name|insert_value }} />
</div>
<div class="level-item buttons are-small">
<button class="button">
<svg class="icon">
<use href="/static/feather-sprite.svg#check-circle" />
</svg>
</button>
{% if let Some(id) = id %}
<button class="button" type="button" hx-get="/clothing/{{ id }}" hx-swap="outerHTML"
hx-target="closest li">
<svg class="icon">
<use href="/static/feather-sprite.svg#x-circle" />
</svg>
</button>
{% else %}
<button class="button" type="button" _="on click remove the closest <li/>">
<svg class="icon">
<use href="/static/feather-sprite.svg#x-circle" />
</svg>
</button>
{% endif %}
</div>
</div>
</div>
</form>
</li>

View File

@ -1,15 +1,23 @@
<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 class="level">
<div class="level-left">
<div class="level-item">
{{ c.name }}
</div>
<div class="level-item buttons are-small">
<button class="button" hx-get="/clothing/edit/{{ c.id }}" hx-swap="outerHTML" hx-target="closest li">
<svg class="icon">
<use href="/static/feather-sprite.svg#edit" />
</svg>
</button>
<button class="button" hx-delete="/clothing/{{ c.id }}" hx-swap="delete"
hx-target="closest li" hx-trigger="confirmed">
<svg class="icon">
<use href="/static/feather-sprite.svg#trash-2" />
</svg>
</button>
</div>
</div>
</div>
</li>

View File

@ -13,6 +13,9 @@
{% for c in clothings %}
{% include "clothing_entry_read.html" %}
{% endfor %}
<li>
<button hx-get="/clothing/new" hx-swap="beforebegin">neue Anzugsordnung +</button>
</li>
</ul>
</div>
</section>