parent
9e6b17b154
commit
123a95277d
@ -0,0 +1,14 @@
|
||||
use axum::debug_handler;
|
||||
use axum::extract::{Path, State};
|
||||
use sqlx::SqlitePool;
|
||||
use askama_axum::{IntoResponse, Response};
|
||||
use crate::db::inventory_item::delete_inventory_item;
|
||||
use crate::error::AppError;
|
||||
|
||||
#[debug_handler]
|
||||
pub async fn delete_item(State(db): State<SqlitePool>, Path(id): Path<i64>) -> Result<Response, AppError> {
|
||||
delete_inventory_item(&db, id).await?;
|
||||
|
||||
let response = format!("Item {} Deleted", id);
|
||||
Ok(response.into_response())
|
||||
}
|
||||
@ -0,0 +1,184 @@
|
||||
use crate::error::AppError;
|
||||
use crate::session::SessionUser;
|
||||
use askama::Template;
|
||||
use askama_axum::{IntoResponse, Response};
|
||||
use axum::extract::{Path, 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::db::inventory_item::{DbInventoryItem, DbInventoryItemEditableFields};
|
||||
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::edit_field_value::{EditBoolField, EditFloatField, EditStringField};
|
||||
use crate::util::form::extractors::{FormBase, FormWithPathVars, ValidForm};
|
||||
use crate::util::form::field_error::FieldError;
|
||||
|
||||
#[derive(Template, Debug, Default)]
|
||||
#[template(path = "item/item-edit-form.html")]
|
||||
pub struct EditItemFormTemplate {
|
||||
pub item_id: i64,
|
||||
pub display_units: Vec<DbDisplayUnit>,
|
||||
pub name: EditStringField,
|
||||
pub display_unit_value: EditStringField,
|
||||
pub reorder_point: EditStringField,
|
||||
pub pims_id: EditStringField,
|
||||
pub vetcove_id: EditStringField,
|
||||
pub allow_fractional_units: EditBoolField,
|
||||
}
|
||||
|
||||
impl EditItemFormTemplate {
|
||||
pub fn base(item: DbInventoryItemEditableFields, display_units: Vec<DbDisplayUnit>) -> Self {
|
||||
let name = EditStringField::new(item.name);
|
||||
let pims_id = EditStringField::from(item.pims_id);
|
||||
let vetcove_id = EditStringField::from(item.vetcove_id);
|
||||
let allow_fractional_units = EditBoolField::new(item.allow_fractional_units);
|
||||
let display_unit_value = EditStringField::new(item.display_unit_abbreviation);
|
||||
|
||||
let precision = if item.allow_fractional_units { 2 } else { 0 };
|
||||
let reorder_point= format!("{:.*}", precision, item.reorder_point);
|
||||
let reorder_point = EditStringField::new(reorder_point);
|
||||
|
||||
Self {
|
||||
item_id: item.id,
|
||||
name,
|
||||
reorder_point,
|
||||
pims_id,
|
||||
vetcove_id,
|
||||
allow_fractional_units,
|
||||
display_unit_value,
|
||||
display_units,
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
#[derive(Deserialize, Debug)]
|
||||
pub struct EditItemFormData {
|
||||
name: String,
|
||||
display_unit: String,
|
||||
reorder_point: f64,
|
||||
pims_id: Option<String>,
|
||||
vetcove_id: Option<String>,
|
||||
#[serde(default, deserialize_with = "deserialize_form_checkbox")]
|
||||
allow_fractional_units: bool,
|
||||
}
|
||||
|
||||
#[async_trait]
|
||||
impl ValidateForm for FormWithPathVars<i64,EditItemFormData> {
|
||||
type ValidationErrorResponse = EditItemFormTemplate;
|
||||
|
||||
async fn validate(self, state: &AppState) -> Result<Self, ValidateFormError<Self::ValidationErrorResponse>> {
|
||||
let item_id = self.path_data;
|
||||
let display_units = db::display_unit::get_display_units(&state.db).await?;
|
||||
let current_values = db::inventory_item::inventory_item_get_by_id_editable_fields(&state.db, item_id).await?;
|
||||
|
||||
let name = EditStringField::new_with_base(
|
||||
self.form_data.name.clone(),
|
||||
current_values.name.clone(),
|
||||
)
|
||||
.invalid_if(|v| v.is_empty(), FieldError::Required);
|
||||
|
||||
let display_unit_value = EditStringField::new_with_base(
|
||||
self.form_data.display_unit.clone(),
|
||||
current_values.display_unit_abbreviation.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)
|
||||
.reset_if_error();
|
||||
|
||||
let reorder_point = EditFloatField::new_with_base(
|
||||
self.form_data.reorder_point.clone(),
|
||||
current_values.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, b| (format!("{:.2}", v), format!("{:.2}", b)));
|
||||
|
||||
let pims_id = EditStringField::new_with_base(
|
||||
self.form_data.pims_id.clone().unwrap_or_default(),
|
||||
current_values.pims_id.unwrap_or_default(),
|
||||
)
|
||||
.invalid_if(|v| v.chars().any(char::is_whitespace), FieldError::ValidIdentifier);
|
||||
|
||||
let vetcove_id = EditStringField::new_with_base(
|
||||
self.form_data.vetcove_id.clone().unwrap_or_default(),
|
||||
current_values.vetcove_id.unwrap_or_default(),
|
||||
)
|
||||
.invalid_if(|v| !v.chars().all(|c| c.is_ascii_digit()), FieldError::ValidIdentifier);
|
||||
|
||||
let allow_fractional_units = EditBoolField::new_with_base(
|
||||
self.form_data.allow_fractional_units,
|
||||
current_values.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 {
|
||||
item_id,
|
||||
display_units,
|
||||
name,
|
||||
display_unit_value,
|
||||
reorder_point,
|
||||
pims_id,
|
||||
vetcove_id,
|
||||
allow_fractional_units,
|
||||
}));
|
||||
}
|
||||
|
||||
|
||||
Ok(self)
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn edit_item_form_post(
|
||||
State(state): State<AppState>,
|
||||
user: SessionUser,
|
||||
Path(id): Path<i64>,
|
||||
form: ValidForm<FormWithPathVars<i64,EditItemFormData>>,
|
||||
) -> Result<Response, AppError> {
|
||||
|
||||
let form_data = &form.form_data;
|
||||
|
||||
db::inventory_item::update_inventory_item(&state.db, id, &form_data.name, form_data.reorder_point,
|
||||
form_data.allow_fractional_units, &form_data.display_unit,
|
||||
&form_data.pims_id, &form_data.vetcove_id, true,
|
||||
).await?;
|
||||
|
||||
let new_base = db::inventory_item::inventory_item_get_by_id_editable_fields(&state.db, id).await?;
|
||||
let display_units = db::display_unit::get_display_units(&state.db).await?;
|
||||
let fresh_form = EditItemFormTemplate::base(new_base, display_units);
|
||||
|
||||
let events = vec![
|
||||
HxEvent::from("form-submit-success"),
|
||||
HxEvent::from("item-updated"),
|
||||
];
|
||||
|
||||
Ok( (
|
||||
HxResponseTrigger::normal(events),
|
||||
fresh_form.into_response()
|
||||
).into_response() )
|
||||
|
||||
}
|
||||
|
||||
#[debug_handler]
|
||||
pub async fn edit_item_form_get(
|
||||
Path(id): Path<i64>,
|
||||
State(state): State<AppState>,
|
||||
) -> Result<Response, AppError> {
|
||||
let item_vars = db::inventory_item::inventory_item_get_by_id_editable_fields(&state.db, id).await?;
|
||||
let display_units = db::display_unit::get_display_units(&state.db).await?;
|
||||
let template = EditItemFormTemplate::base(item_vars, display_units);
|
||||
Ok(template.into_response())
|
||||
}
|
||||
@ -0,0 +1,132 @@
|
||||
use std::fmt::{Debug, Display};
|
||||
use crate::util::form::field_error::FieldError;
|
||||
|
||||
/**
|
||||
A sum type for use with forms and form validation. Similar to a basic field_value,
|
||||
but contains a base value that can be compared to and reverted to. The base value is
|
||||
assumed to be "good" in terms of validation, meaning the Error refers to the current
|
||||
value rather than the base value.
|
||||
*/
|
||||
|
||||
pub struct EditFieldValue<Value, Error> {
|
||||
pub value: Value,
|
||||
pub base: Value,
|
||||
pub error: Option<Error>,
|
||||
}
|
||||
|
||||
pub type EditStringField = EditFieldValue<String, FieldError>;
|
||||
pub type EditBoolField = EditFieldValue<bool, FieldError>;
|
||||
pub type EditFloatField = EditFieldValue<f64, FieldError>;
|
||||
|
||||
impl<Value, Error> EditFieldValue<Value, Error>
|
||||
where Value: Default + Clone + PartialEq, Error: ToString
|
||||
{
|
||||
pub fn new(value: Value) -> Self {
|
||||
let base = value.clone();
|
||||
Self { value, base, error: None }
|
||||
}
|
||||
|
||||
pub fn new_with_base(value: Value, base: Value) -> Self {
|
||||
Self { value, base, error: None }
|
||||
}
|
||||
|
||||
pub fn set_error(&mut self, error: Error) {
|
||||
self.error = Some(error);
|
||||
}
|
||||
|
||||
pub fn is_error(&self) -> bool {
|
||||
self.error.is_some()
|
||||
}
|
||||
|
||||
pub fn invalid_if<I>(mut self, is_invalid: I, err: Error ) -> Self
|
||||
where I: FnOnce(&Value) -> bool {
|
||||
if is_invalid(&self.value) {
|
||||
self.set_error(err);
|
||||
}
|
||||
self
|
||||
}
|
||||
|
||||
pub fn invalid_if_then<I, E>(mut self, is_invalid: I, err_if: E ) -> Self
|
||||
where I: FnOnce(&Value) -> bool, E: FnOnce() -> Error {
|
||||
if is_invalid(&self.value) {
|
||||
self.set_error(err_if());
|
||||
}
|
||||
self
|
||||
}
|
||||
|
||||
pub fn with_base(mut self, base: Value) -> Self {
|
||||
self.base = base;
|
||||
self
|
||||
}
|
||||
|
||||
pub fn is_changed(&self) -> bool {
|
||||
self.value != self.base
|
||||
}
|
||||
|
||||
pub fn reset(mut self) -> Self {
|
||||
self.value = self.base.clone();
|
||||
self
|
||||
}
|
||||
|
||||
pub fn reset_if_error(mut self) -> Self {
|
||||
if self.is_error() { self.reset() } else { self }
|
||||
}
|
||||
|
||||
pub fn map<F, V>(self, f: F) -> EditFieldValue<V, Error>
|
||||
where F: FnOnce(Value, Value) -> (V, V) {
|
||||
let (value, base) = f(self.value, self.base);
|
||||
EditFieldValue::<V, Error> {
|
||||
value,
|
||||
base,
|
||||
error: self.error,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn with_error(mut self, error: Error) -> Self {
|
||||
self.set_error(error);
|
||||
self
|
||||
}
|
||||
|
||||
pub fn error_string(&self) -> String {
|
||||
match self.error {
|
||||
Some(ref error) => error.to_string(),
|
||||
None => "Unknown".to_string(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<Value, Error> Default for EditFieldValue<Value, Error>
|
||||
where Value: Default + Clone + PartialEq, Error: ToString
|
||||
{
|
||||
fn default() -> Self {
|
||||
Self::new(Value::default())
|
||||
}
|
||||
}
|
||||
|
||||
impl<Value, Error> Debug for EditFieldValue<Value, Error>
|
||||
where Value: Debug,
|
||||
Error: Debug {
|
||||
fn fmt(&self, fmt: &mut ::std::fmt::Formatter) -> Result<(), ::std::fmt::Error> {
|
||||
fmt.debug_struct("EditFieldValue")
|
||||
.field("value", &self.value)
|
||||
.field("error", &self.error)
|
||||
.finish()
|
||||
}
|
||||
}
|
||||
|
||||
impl<Value, Error> From<Option<Value>> for EditFieldValue<Value, Error>
|
||||
where Value: Default + Clone + PartialEq, Error: ToString {
|
||||
fn from(value: Option<Value>) -> Self {
|
||||
Self::new(value.unwrap_or_default())
|
||||
}
|
||||
}
|
||||
|
||||
impl<Value, Error> From<Result<Value, Error>> for EditFieldValue<Value, Error>
|
||||
where Value: Default + Clone + PartialEq, Error: ToString {
|
||||
fn from(value: Result<Value, Error>) -> Self {
|
||||
match value {
|
||||
Ok(value) => Self::new(value),
|
||||
Err(error) => Self::default().with_error(error),
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,148 @@
|
||||
<form
|
||||
id="item-create"
|
||||
hx-post="/item/{{item_id}}/edit"
|
||||
hx-target="this"
|
||||
hx-swap="outerHTML"
|
||||
x-on:htmx:response-error="$dispatch('notice', {type: 'error', text: 'Unknown error'})"
|
||||
x-on:form-submit-success="$dispatch('notice', {type: 'info', text: 'Changes saved'})"
|
||||
>
|
||||
<div class="mb-5 grid grid-cols-6 gap-4 p-2">
|
||||
<div class="col-span-6">
|
||||
<label for="name" class="mb-2 block text-sm font-medium">Name</label>
|
||||
<input
|
||||
type="text"
|
||||
id="name"
|
||||
name="name"
|
||||
class="block w-11/12 p-2 rounded-lg border border-neutral-900 text-sm text-neutral-900 focus:border-paynes-gray focus:ring-paynes-gray dark:text-slate-100"
|
||||
aria-label="name"
|
||||
value="{{ name.value }}"
|
||||
required
|
||||
{% if name.is_error() -%}
|
||||
aria-invalid="true"
|
||||
aria-describedby="invalid-name"
|
||||
{% endif -%}
|
||||
/>
|
||||
{% if name.is_error() -%}
|
||||
<small id="invalid-name" class="block mt-2 text-sm text-cerise">
|
||||
{{ name.error_string() }}
|
||||
</small>
|
||||
{% endif -%}
|
||||
</div>
|
||||
|
||||
<div class="col-span-2">
|
||||
<label for="reorder_point" class="mb-2 block text-sm font-medium">Reorder Point</label>
|
||||
<input
|
||||
type="number"
|
||||
id="reorder_point"
|
||||
name="reorder_point"
|
||||
step="0.01"
|
||||
class="block w-3/4 p-2 rounded-lg border border-neutral-900 text-sm text-neutral-900 focus:border-paynes-gray focus:ring-paynes-gray dark:text-slate-100"
|
||||
aria-label="reorder point"
|
||||
value="{{ reorder_point.value }}"
|
||||
{% if reorder_point.is_error() -%}
|
||||
aria-invalid="true"
|
||||
aria-describedby="invalid-reorder-point"
|
||||
{% endif -%}
|
||||
/>
|
||||
{% if reorder_point.is_error() -%}
|
||||
<small id="invalid-reorder-point" class="block mt-2 text-sm text-cerise">
|
||||
{{ reorder_point.error_string() }}
|
||||
</small>
|
||||
{% endif -%}
|
||||
</div>
|
||||
|
||||
<div class="col-span-2">
|
||||
<label for="display_unit" class="mb-2 block text-sm font-medium">Unit</label>
|
||||
<select
|
||||
id="display_unit"
|
||||
name="display_unit"
|
||||
class="block rounded-lg border border-neutral-900 p-2 text-sm text-neutral-900 focus:border-paynes-gray focus:ring-paynes-gray dark:text-slate-100"
|
||||
aria-label="name"
|
||||
required
|
||||
{% if display_unit_value.is_error() -%}
|
||||
aria-invalid="true"
|
||||
aria-describedby="invalid-display-unit"
|
||||
{% endif -%}
|
||||
>
|
||||
{% for unit in display_units -%}
|
||||
<option value="{{ unit.abbreviation }}" {% if unit.abbreviation == display_unit_value.value %} selected {% endif %}>{{ unit.unit }}</option>
|
||||
{% endfor %}
|
||||
</select>
|
||||
{% if display_unit_value.is_error() -%}
|
||||
<small id="invalid-display-unit" class="block mt-2 text-sm text-cerise">
|
||||
{{ display_unit_value.error_string() }}
|
||||
</small>
|
||||
{% endif -%}
|
||||
</div>
|
||||
|
||||
<div class="col-span-2">
|
||||
<label for="allow_fractional_units" class="block mb-2 text-sm font-medium">Fractional</label>
|
||||
<input
|
||||
type="checkbox"
|
||||
id="allow_fractional_units"
|
||||
name="allow_fractional_units"
|
||||
class="block w-4 h-4 text-dark-cyan border-slate-100 rounded-sm focus:ring-space-cadet focus:ring-2"
|
||||
{% if allow_fractional_units.value -%} checked {% endif -%}
|
||||
/>
|
||||
</div>
|
||||
|
||||
|
||||
<div class="col-span-3">
|
||||
<label for="pims_id" class="mb-2 block text-sm font-medium">PIMS Id</label>
|
||||
<input
|
||||
type="text"
|
||||
id="pims_id"
|
||||
name="pims_id"
|
||||
class="block p-2 rounded-lg border border-neutral-900 text-sm text-neutral-900 focus:border-paynes-gray focus:ring-paynes-gray dark:text-slate-100"
|
||||
value="{{ pims_id.value }}"
|
||||
aria-label="pims id"
|
||||
{% if pims_id.is_error() -%}
|
||||
aria-invalid="true"
|
||||
aria-describedby="invalid-pims-id"
|
||||
{% endif -%}
|
||||
/>
|
||||
{% if pims_id.is_error() -%}
|
||||
<small id="invalid-pims-id" class="block mt-2 text-sm text-cerise">
|
||||
{{ pims_id.error_string() }}
|
||||
</small>
|
||||
{% endif -%}
|
||||
</div>
|
||||
|
||||
<div class="col-span-3">
|
||||
<label for="vetcove_id" class="mb-2 block text-sm font-medium">Vetcove Id</label>
|
||||
<input
|
||||
type="text"
|
||||
id="vetcove_id"
|
||||
name="vetcove_id"
|
||||
class="block p-2 rounded-lg border border-neutral-900 text-sm text-neutral-900 focus:border-paynes-gray focus:ring-paynes-gray dark:text-slate-100"
|
||||
value="{{ vetcove_id.value }}"
|
||||
aria-label="vetcove id"
|
||||
{% if vetcove_id.is_error() -%}
|
||||
aria-invalid="true"
|
||||
aria-describedby="invalid-vetcove-id"
|
||||
{% endif -%}
|
||||
/>
|
||||
{% if vetcove_id.is_error() -%}
|
||||
<small id="invalid-vetcove-id" class="block mt-2 text-sm text-cerise">
|
||||
{{ vetcove_id.error_string() }}
|
||||
</small>
|
||||
{% endif -%}
|
||||
</div>
|
||||
<div class="col-span-4">
|
||||
<button
|
||||
class="mb-2 me-2 rounded-lg bg-english-violet px-5 py-2.5 text-sm font-medium text-slate-100 hover:bg-dark-cyan focus:outline-none focus:ring-4 focus:ring-dark-cyan"
|
||||
>
|
||||
Save
|
||||
</button>
|
||||
</div>
|
||||
<div class="col-span-4">
|
||||
|
||||
<button
|
||||
class="mb-2 me-2 rounded-lg bg-cerise px-5 py-2.5 text-sm font-medium text-neutral-900 hover:bg-orchid-pink focus:outline-none focus:ring-4 focus:ring-dark-cyan"
|
||||
hx-delete="/item/{{item_id}}"
|
||||
>
|
||||
Delete
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
Loading…
Reference in new issue