use crate::error::AppError; use crate::session::SessionUser; use askama::Template; use askama_axum::{IntoResponse, Response}; use axum::extract::State; use axum::{async_trait, debug_handler}; use axum_htmx::{HxEvent, HxResponseTrigger}; use serde::Deserialize; use crate::app::routes::AppState; use crate::db::display_unit::DbDisplayUnit; use crate::db; use crate::util::form::field_error::FieldError; use crate::util::form::field_value::{BoolField, FloatField, StringField}; use crate::util::form::validate_form::{ValidateForm, ValidateFormError}; use crate::util::form::helpers::deserialize_form_checkbox; use crate::util::form::base_response::BaseResponse; use crate::util::form::extractors::{FormBase, ValidForm}; #[derive(Template, Debug, Default)] #[template(path = "item/item-create-form.html")] pub struct CreateItemFormTemplate { pub display_units: Vec, pub name: StringField, pub display_unit_value: StringField, pub reorder_point: StringField, pub pims_id: StringField, pub vetcove_id: StringField, pub allow_fractional_units: BoolField, } #[async_trait] impl BaseResponse for CreateItemFormTemplate { type ResponseType = Self; async fn base_response(state: &AppState) -> Result { let db = &state.db; let display_units = db::display_unit::get_display_units(&db).await?; Ok(Self { display_units, ..Default::default() }) } } #[derive(Deserialize, Debug)] pub struct CreateItemFormData { name: String, display_unit: String, reorder_point: f64, pims_id: Option, vetcove_id: Option, #[serde(default, deserialize_with = "deserialize_form_checkbox")] allow_fractional_units: bool, } #[async_trait] impl ValidateForm for FormBase { type ValidationErrorResponse = CreateItemFormTemplate; async fn validate(self, state: &AppState) -> Result> { let display_units = db::display_unit::get_display_units(&state.db).await?; let name = StringField::new(self.form_data.name.clone()) .invalid_if(|v| v.is_empty(), FieldError::Required); let display_unit_value = StringField::new(self.form_data.display_unit.clone()) .invalid_if(|v| v.is_empty(), FieldError::Required) .invalid_if(|v| !display_units.iter().any( |x| x.abbreviation.eq(&self.form_data.display_unit)), FieldError::SelectOption) .clear_value_if_error(); let reorder_point = FloatField::new(self.form_data.reorder_point.clone()) .invalid_if(|v| v.is_nan() || v.is_infinite() || v.is_sign_negative(), FieldError::PositiveNumber) .invalid_if(|v| !(self.form_data.allow_fractional_units || v.fract() == 0.0), FieldError::WholeNumber) .map(|v| format!("{:.2}", v)); let pims_id = StringField::new(self.form_data.pims_id.clone().unwrap_or_default()) .invalid_if(|v| v.chars().any(char::is_whitespace), FieldError::ValidIdentifier); let vetcove_id = StringField::new(self.form_data.vetcove_id.clone().unwrap_or_default()) .invalid_if(|v| !v.chars().all(|c| c.is_ascii_digit()), FieldError::ValidIdentifier); let allow_fractional_units = BoolField::new(self.form_data.allow_fractional_units); if name.is_error() || display_unit_value.is_error() || reorder_point.is_error() || pims_id.is_error() || vetcove_id.is_error() { return Err(ValidateFormError::ValidationError( Self::ValidationErrorResponse { display_units, name, display_unit_value, reorder_point, pims_id, vetcove_id, allow_fractional_units, })); } Ok(self) } } #[debug_handler] pub async fn create_item_form_post( State(state): State, user: SessionUser, form: ValidForm>, ) -> Result { let form_data = &form.form_data; let _new_id = db::inventory_item::add_inventory_item(&state.db, &form_data.name, form_data.reorder_point, form_data.allow_fractional_units, &form_data.display_unit, &form_data.pims_id, &form_data.vetcove_id, ).await?; let fresh_form = CreateItemFormTemplate::base_response(&state).await?; let events = vec![ HxEvent::from("form-submit-success"), HxEvent::from("new-item"), ]; Ok( ( HxResponseTrigger::normal(events), fresh_form.into_response() ).into_response() ) } #[debug_handler] pub async fn create_item_form_get( State(state): State, ) -> Result { Ok(CreateItemFormTemplate::base_response(&state).await?.into_response()) }