// SPDX-FileCopyrightText: 2024 Simon Bruder // // 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, path: web::Path, user: Identity, ) -> actix_web::Result { 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::() .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::>().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, user: Identity) -> actix_web::Result { 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, pub description: String, } #[derive(Debug, Deserialize)] pub struct NewItemClassFormPrefilled { pub name: Option, pub parent: Option, pub description: Option, } #[get("/item-classes/add")] async fn add_form( pool: web::Data, form: web::Query, user: Identity, ) -> actix_web::Result { 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, pool: web::Data, _user: Identity, ) -> actix_web::Result { 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, path: web::Path, user: Identity, ) -> actix_web::Result { 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, path: web::Path, data: web::Form, _user: Identity, ) -> actix_web::Result { 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, path: web::Path, _user: Identity, ) -> actix_web::Result { let id = path.into_inner(); manage::item_class::delete(&pool, id) .await .map_err(error::ErrorInternalServerError)?; Ok(web::Redirect::to("/item-classes").see_other()) }