li7y/src/frontend/item_class.rs
Simon Bruder 7defae7931
Decouple item (class) form from model
This makes it possible to introduce form-only fields without affecting
the model.
2024-07-19 00:06:27 +02:00

422 lines
14 KiB
Rust
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

// SPDX-FileCopyrightText: 2024 Simon Bruder <simon@sbruder.de>
//
// SPDX-License-Identifier: AGPL-3.0-or-later
use actix_identity::Identity;
use actix_web::{error, get, post, web, Responder};
use maud::html;
use serde::Deserialize;
use sqlx::PgPool;
use uuid::Uuid;
use super::templates::helpers::{Colour, ItemName, PageAction, PageActionGroup, PageActionMethod};
use super::templates::{self, datalist, forms, TemplateConfig};
use crate::label::LabelPreset;
use crate::manage;
use crate::models::*;
pub fn config(cfg: &mut web::ServiceConfig) {
cfg.service(show)
.service(list)
.service(add_form)
.service(add)
.service(edit_form)
.service(edit)
.service(delete);
}
#[get("/item-class/{id}")]
async fn show(
pool: web::Data<PgPool>,
path: web::Path<Uuid>,
user: Identity,
) -> actix_web::Result<impl Responder> {
let id = path.into_inner();
let item_class = manage::item_class::get(&pool, id)
.await
.map_err(error::ErrorInternalServerError)?;
// TODO: Once async closures are stable, use map_or on item_class.parent instead
let parent = match item_class.parent {
Some(id) => manage::item_class::get(&pool, id)
.await
.map(Some)
.map_err(error::ErrorInternalServerError)?,
None => None,
};
let children = manage::item_class::children(&pool, id)
.await
.map_err(error::ErrorInternalServerError)?;
let items = manage::item_class::items(&pool, id)
.await
.map_err(error::ErrorInternalServerError)?;
let mut title = item_class.name.clone();
title.push_str(" Item Details");
let mut page_actions = vec![
(PageActionGroup::Button {
action: PageAction {
method: PageActionMethod::Get,
target: format!("/items/add?class={}", item_class.id),
name: "Add Item".to_string(),
},
colour: Colour::Success,
}),
];
if item_class.parent.is_none() {
page_actions.push(PageActionGroup::Button {
action: PageAction {
method: PageActionMethod::Get,
target: format!("/item-classes/add?parent={}", item_class.id),
name: "Add Child".to_string(),
},
colour: Colour::Primary,
});
}
page_actions.push(PageActionGroup::Button {
action: PageAction {
method: PageActionMethod::Get,
target: format!("/item-class/{}/edit", item_class.id),
name: "Edit".to_string(),
},
colour: Colour::Warning,
});
page_actions.push(PageActionGroup::Button {
action: PageAction {
method: PageActionMethod::Post,
target: format!("/item-class/{}/delete", item_class.id),
name: "Delete".to_string(),
},
colour: Colour::Danger,
});
Ok(templates::base(
TemplateConfig {
path: &format!("/item-class/{}", item_class.id),
title: Some(&title),
page_title: Some(Box::new(item_class.name.clone())),
page_actions,
user: Some(user),
..Default::default()
},
html! {
table .table {
tr {
th { "UUID" }
td { (item_class.id) }
}
tr {
th { "Name" }
td { (item_class.name) }
}
@if let Some(parent) = parent {
tr {
th { "Parent" }
td { a href={ "/item-class/" (parent.id) } { (parent.name) } }
}
}
tr {
th { "Description" }
td style="white-space: pre-wrap" { (item_class.description) }
}
}
@if !children.is_empty() {
h3 .mt-4 { "Children" }
ul {
@for child in children {
li {
a href={ "/item-class/" (child.id) } { (child.name) }
}
}
}
}
@if !items.is_empty() {
div .d-flex.justify-content-between.mt-4 {
div {
h3 { "Items" }
}
div {
(PageActionGroup::Dropdown {
name: "Generate Labels".to_string(),
actions: enum_iterator::all::<LabelPreset>()
.map(|preset| PageAction {
method: PageActionMethod::Get,
target: format!(
"/labels/generate?preset={}&ids={}",
&serde_variant::to_variant_name(&preset).unwrap(),
items.iter().map(|i| i.id.to_string()).collect::<Vec<String>>().join(",")
),
name: preset.to_string(),
})
.collect(),
colour: Colour::Primary,
})
}
}
ul {
@for item in items {
li {
a href={ "/item/" (item.id) } { (ItemName::new(&item, &item_class).terse()) }
}
}
}
}
},
))
}
#[get("/item-classes")]
async fn list(pool: web::Data<PgPool>, user: Identity) -> actix_web::Result<impl Responder> {
let item_classes_ids = sqlx::query_scalar!("SELECT id FROM item_classes ORDER BY created_at")
.fetch_all(pool.as_ref())
.await
.map_err(error::ErrorInternalServerError)?;
let item_classes = manage::item_class::get_all_as_map(&pool)
.await
.map_err(error::ErrorInternalServerError)?;
Ok(templates::base(
TemplateConfig {
path: "/item-classes",
title: Some("Item Class List"),
page_title: Some(Box::new("Item Class List")),
page_actions: vec![
(PageActionGroup::Button {
action: PageAction {
method: PageActionMethod::Get,
target: "/item-classes/add".to_string(),
name: "Add".to_string(),
},
colour: Colour::Success,
}),
],
user: Some(user),
..Default::default()
},
html! {
table .table {
thead {
tr {
th { "Name" }
th { "Parents" }
}
}
tbody {
@for item_class in item_classes_ids {
@let item_class = item_classes.get(&item_class).unwrap();
tr {
td { a href={ "/item-class/" (item_class.id) } { (item_class.name) } }
td {
@if let Some(parent) = item_class.parent {
@let parent = item_classes.get(&parent).unwrap();
a href={ "/item-class/" (parent.id) } { (parent.name) }
} @else {
"-"
}
}
}
}
}
}
},
))
}
#[derive(Debug, Deserialize)]
pub struct NewItemClassForm {
pub name: String,
pub parent: Option<Uuid>,
pub description: String,
}
#[derive(Debug, Deserialize)]
pub struct NewItemClassFormPrefilled {
pub name: Option<String>,
pub parent: Option<Uuid>,
pub description: Option<String>,
}
#[get("/item-classes/add")]
async fn add_form(
pool: web::Data<PgPool>,
form: web::Query<NewItemClassFormPrefilled>,
user: Identity,
) -> actix_web::Result<impl Responder> {
let datalist_item_classes = datalist::item_classes(&pool)
.await
.map_err(error::ErrorInternalServerError)?;
Ok(templates::base(
TemplateConfig {
path: "/items-classes/add",
title: Some("Add Item Class"),
page_title: Some(Box::new("Add Item Class")),
datalists: vec![&datalist_item_classes],
user: Some(user),
..Default::default()
},
html! {
form method="POST" {
(forms::InputGroup {
r#type: forms::InputType::Text,
name: "name",
title: "Name",
required: true,
value: form.name.clone(),
..Default::default()
})
(forms::InputGroup {
r#type: forms::InputType::Text,
name: "parent",
title: "Parent",
optional: true,
disabled: form.parent.is_none(),
value: form.parent.map(|id| id.to_string()),
datalist: Some(&datalist_item_classes),
..Default::default()
})
(forms::InputGroup {
r#type: forms::InputType::Textarea,
name: "description",
title: "Description ",
value: form.description.clone(),
..Default::default()
})
button .btn.btn-primary type="submit" { "Add" }
}
},
))
}
#[post("/item-classes/add")]
async fn add(
data: web::Form<NewItemClassForm>,
pool: web::Data<PgPool>,
_user: Identity,
) -> actix_web::Result<impl Responder> {
let data = data.into_inner();
let item = manage::item_class::add(
&pool,
NewItemClass {
name: data.name,
parent: data.parent,
description: data.description,
},
)
.await
.map_err(error::ErrorInternalServerError)?;
Ok(web::Redirect::to("/item-class/".to_owned() + &item.id.to_string()).see_other())
}
#[get("/item-class/{id}/edit")]
async fn edit_form(
pool: web::Data<PgPool>,
path: web::Path<Uuid>,
user: Identity,
) -> actix_web::Result<impl Responder> {
let id = path.into_inner();
let item_class = manage::item_class::get(&pool, id)
.await
.map_err(error::ErrorInternalServerError)?;
let datalist_item_classes = datalist::item_classes(&pool)
.await
.map_err(error::ErrorInternalServerError)?;
let mut title = item_class.name.clone();
title.push_str(" Item Details");
Ok(templates::base(
TemplateConfig {
path: &format!("/items-class/{}/add", id),
title: Some(&title),
page_title: Some(Box::new(item_class.name.clone())),
datalists: vec![&datalist_item_classes],
user: Some(user),
..Default::default()
},
html! {
form method="POST" {
(forms::InputGroup {
r#type: forms::InputType::Text,
name: "id",
title: "UUID",
disabled: true,
required: true,
value: Some(item_class.id.to_string()),
..Default::default()
})
(forms::InputGroup {
r#type: forms::InputType::Text,
name: "name",
title: "Name",
required: true,
value: Some(item_class.name),
..Default::default()
})
(forms::InputGroup {
r#type: forms::InputType::Text,
name: "parent",
title: "Parent",
optional: true,
disabled: item_class.parent.is_none(),
value: item_class.parent.map(|id| id.to_string()),
datalist: Some(&datalist_item_classes),
..Default::default()
})
(forms::InputGroup {
r#type: forms::InputType::Textarea,
name: "description",
title: "Description ",
value: Some(item_class.description),
..Default::default()
})
button .btn.btn-primary type="submit" { "Edit" }
}
},
))
}
#[post("/item-class/{id}/edit")]
async fn edit(
pool: web::Data<PgPool>,
path: web::Path<Uuid>,
data: web::Form<NewItemClass>,
_user: Identity,
) -> actix_web::Result<impl Responder> {
let id = path.into_inner();
let item_class = manage::item_class::update(&pool, id, data.into_inner())
.await
.map_err(error::ErrorInternalServerError)?;
Ok(web::Redirect::to("/item-class/".to_owned() + &item_class.id.to_string()).see_other())
}
#[post("/item-class/{id}/delete")]
async fn delete(
pool: web::Data<PgPool>,
path: web::Path<Uuid>,
_user: Identity,
) -> actix_web::Result<impl Responder> {
let id = path.into_inner();
manage::item_class::delete(&pool, id)
.await
.map_err(error::ErrorInternalServerError)?;
Ok(web::Redirect::to("/item-classes").see_other())
}