// SPDX-FileCopyrightText: 2024 Simon Bruder // // SPDX-License-Identifier: AGPL-3.0-or-later use actix_web::{error, get, post, web, Responder}; use maud::html; use uuid::Uuid; use super::templates::{self, forms, TemplateConfig}; use crate::manage; use crate::models::*; use crate::DbPool; const FORM_ENSURE_PARENT: templates::helpers::Js = templates::helpers::Js::Inline( r#" (() => { document.getElementById("type").addEventListener("change", e => { let parentInput = document.getElementById("parent") switch (e.target.value) { case "generic": parentInput.disabled = true parentInput.value = "" break case "specific": parentInput.disabled = false break default: console.error("invalid type!") } }) })()"#, ); pub fn config(cfg: &mut web::ServiceConfig) { cfg.service(show_item_class) .service(list_item_classes) .service(add_item_class) .service(add_item_class_post) .service(edit_item_class) .service(edit_item_class_post); } #[get("/item-class/{id}")] async fn show_item_class( pool: web::Data, path: web::Path, ) -> actix_web::Result { let id = path.into_inner(); let item_class = manage::item_class::get(&mut pool.clone().get().await.unwrap(), 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(&mut pool.get().await.unwrap(), id) .await .map(Some) .map_err(error::ErrorInternalServerError)?, None => None, }; let mut title = item_class.name.clone(); title.push_str(" – Item Details"); 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: vec![ (templates::helpers::PageAction { href: &format!("/item-class/{}/edit", item_class.id), name: "Edit", }), ], ..Default::default() }, html! { table .table { tr { th { "UUID" } td { (item_class.id) } } tr { th { "Name" } td { (item_class.name) } } tr { th { "Type" } td { (item_class.r#type) } } @if let Some(parent) = parent { tr { th { "Parent" } td { a href={ "/item-class/" (parent.id) } { (parent.name) } } } } } }, )) } #[get("/item-classes")] async fn list_item_classes(pool: web::Data) -> actix_web::Result { let item_classes = manage::item_class::get_all_as_map(&mut pool.get().await.unwrap()) .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![ (templates::helpers::PageAction { href: "/item-classes/add", name: "Add", }), ], ..Default::default() }, html! { table .table { thead { tr { th { "Name" } th { "Parents" } } } tbody { @for item_class in item_classes.values() { 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 { "-" } } } } } } }, )) } #[get("/item-classes/add")] async fn add_item_class() -> actix_web::Result { Ok(templates::base( TemplateConfig { path: "/items-classes/add", title: Some("Add Item Class"), page_title: Some(Box::new("Add Item Class")), extra_js: vec![FORM_ENSURE_PARENT], ..Default::default() }, html! { form method="POST" { (forms::InputGroup { r#type: forms::InputType::Text, name: "name", title: "Name", required: true, ..Default::default() }) // TODO: drop type in favour of determining it on whether parent is set .mb-3 { label .form-label for="type" { "Type" } select .form-select #type name="type" required { @for variant in ItemClassType::VARIANTS { option { (variant) } } } } .mb-3 { label .form-label for="parent" { "Parent" } input .form-control #parent type="text" name="parent" disabled; } button .btn.btn-primary type="submit" { "Add" } } }, )) } #[post("/item-classes/add")] async fn add_item_class_post( data: web::Form, pool: web::Data, ) -> actix_web::Result { let item = manage::item_class::add(&mut pool.get().await.unwrap(), data.into_inner()) .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_item_class( pool: web::Data, path: web::Path, ) -> actix_web::Result { let id = path.into_inner(); let item_class = manage::item_class::get(&mut pool.get().await.unwrap(), id) .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())), extra_js: vec![FORM_ENSURE_PARENT], ..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().as_str()), }) (forms::InputGroup { r#type: forms::InputType::Text, name: "name", title: "Name", required: true, value: Some(&item_class.name), ..Default::default() }) // TODO: drop type in favour of determining it on whether parent is set .mb-3 { label .form-label for="type" { "Type" } select .form-select #type name="type" required { @for variant in ItemClassType::VARIANTS { option selected[variant == item_class.r#type] { (variant) } } } } .mb-3 { label .form-label for="parent" { "Parent" } input .form-control #parent type="text" name="parent" disabled[item_class.parent.is_none()] value=[item_class.parent]; } button .btn.btn-primary type="submit" { "Edit" } } }, )) } #[post("/item-class/{id}/edit")] async fn edit_item_class_post( pool: web::Data, path: web::Path, data: web::Form, ) -> actix_web::Result { let id = path.into_inner(); let item_class = manage::item_class::update(&mut pool.get().await.unwrap(), id, data.into_inner()) .await .map_err(error::ErrorInternalServerError)?; Ok(web::Redirect::to("/item-class/".to_owned() + &item_class.id.to_string()).see_other()) }