some htmx and alpine
This commit is contained in:
@@ -1,4 +1,4 @@
|
|||||||
use maud::{html, Markup};
|
use maud::{html, Markup, PreEscaped};
|
||||||
|
|
||||||
use crate::models::*;
|
use crate::models::*;
|
||||||
use crate::ClientState;
|
use crate::ClientState;
|
||||||
@@ -312,12 +312,154 @@ impl InventoryItemList {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub struct InventoryNewItemFormName;
|
||||||
|
|
||||||
|
impl InventoryNewItemFormName {
|
||||||
|
pub fn build(value: Option<&str>, error: bool) -> Markup {
|
||||||
|
html!(
|
||||||
|
div
|
||||||
|
."grid"
|
||||||
|
."grid-cols-[2fr,3fr]"
|
||||||
|
."justify-items-center"
|
||||||
|
."items-center"
|
||||||
|
hx-post="/inventory/item/name/validate"
|
||||||
|
hx-trigger="input delay:1s, every 5s"
|
||||||
|
hx-params="new-item-name"
|
||||||
|
hx-swap="outerHTML"
|
||||||
|
{
|
||||||
|
label for="name" .font-bold { "Name" }
|
||||||
|
input
|
||||||
|
type="text"
|
||||||
|
id="new-item-name"
|
||||||
|
name="new-item-name"
|
||||||
|
x-on:input="(e) => {save_active = inventory_new_item_check_input()}"
|
||||||
|
."block"
|
||||||
|
."w-full"
|
||||||
|
."p-2"
|
||||||
|
."bg-gray-50"
|
||||||
|
."border-2"
|
||||||
|
."border-red-500"[error]
|
||||||
|
."border-gray-300"[!error]
|
||||||
|
."rounded"
|
||||||
|
."focus:outline-none"
|
||||||
|
."focus:bg-white"
|
||||||
|
."focus:border-purple-500"[!error]
|
||||||
|
value=[value]
|
||||||
|
;
|
||||||
|
@if error {
|
||||||
|
div
|
||||||
|
."col-start-2"
|
||||||
|
."text-sm"
|
||||||
|
."text-red-500"
|
||||||
|
{ "name already exists" }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub struct InventoryNewItemFormWeight;
|
||||||
|
|
||||||
|
impl InventoryNewItemFormWeight {
|
||||||
|
pub fn build() -> Markup {
|
||||||
|
html!(
|
||||||
|
div
|
||||||
|
."grid"
|
||||||
|
."grid-cols-[2fr,3fr]"
|
||||||
|
."justify-items-center"
|
||||||
|
."items-center"
|
||||||
|
{
|
||||||
|
label for="weight" .font-bold { "Weight" }
|
||||||
|
input
|
||||||
|
type="number"
|
||||||
|
id="new-item-weight"
|
||||||
|
name="new-item-weight"
|
||||||
|
min="0"
|
||||||
|
x-on:input="(e) => {
|
||||||
|
save_active = inventory_new_item_check_input();
|
||||||
|
weight_error = !check_weight();
|
||||||
|
}"
|
||||||
|
x-bind:class="weight_error && 'border-red-500' || 'border-gray-300 focus:border-purple-500'"
|
||||||
|
."block"
|
||||||
|
."w-full"
|
||||||
|
."p-2"
|
||||||
|
."bg-gray-50"
|
||||||
|
."border-2"
|
||||||
|
."rounded"
|
||||||
|
."focus:outline-none"
|
||||||
|
."focus:bg-white"
|
||||||
|
{}
|
||||||
|
span
|
||||||
|
// x-on produces some errors, this works as well
|
||||||
|
x-bind:class="!weight_error && 'hidden'"
|
||||||
|
."col-start-2"
|
||||||
|
."text-sm"
|
||||||
|
."text-red-500"
|
||||||
|
{ "invalid input" }
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub struct InventoryNewItemFormCategory;
|
||||||
|
|
||||||
|
impl InventoryNewItemFormCategory {
|
||||||
|
pub fn build(state: &ClientState, categories: &Vec<Category>) -> Markup {
|
||||||
|
html!(
|
||||||
|
div
|
||||||
|
."grid"
|
||||||
|
."grid-cols-[2fr,3fr]"
|
||||||
|
."justify-items-center"
|
||||||
|
."items-center"
|
||||||
|
{
|
||||||
|
label for="item-category" .font-bold ."w-1/2" .text-center { "Category" }
|
||||||
|
select
|
||||||
|
id="new-item-category-id"
|
||||||
|
name="new-item-category-id"
|
||||||
|
."block"
|
||||||
|
."w-full"
|
||||||
|
."p-2"
|
||||||
|
."bg-gray-50"
|
||||||
|
."border-2"
|
||||||
|
."border-gray-300"
|
||||||
|
."rounded"
|
||||||
|
."focus:outline-none"
|
||||||
|
."focus:bg-white"
|
||||||
|
."focus:border-purple-500"
|
||||||
|
autocomplete="off" // https://stackoverflow.com/a/10096033
|
||||||
|
{
|
||||||
|
@for category in categories {
|
||||||
|
option value=(category.id) selected[state.active_category_id.map_or(false, |id| id == category.id)] {
|
||||||
|
(category.name)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
pub struct InventoryNewItemForm;
|
pub struct InventoryNewItemForm;
|
||||||
|
|
||||||
impl InventoryNewItemForm {
|
impl InventoryNewItemForm {
|
||||||
pub fn build(state: &ClientState, categories: &Vec<Category>) -> Markup {
|
pub fn build(state: &ClientState, categories: &Vec<Category>) -> Markup {
|
||||||
html!(
|
html!(
|
||||||
|
script {
|
||||||
|
(PreEscaped("
|
||||||
|
function inventory_new_item_check_input() {
|
||||||
|
return document.getElementById('new-item-name').value.length != 0
|
||||||
|
&& is_positive_integer(document.getElementById('new-item-weight').value)
|
||||||
|
}
|
||||||
|
function check_weight() {
|
||||||
|
return document.getElementById('new-item-weight').validity.valid;
|
||||||
|
}
|
||||||
|
"))
|
||||||
|
}
|
||||||
form
|
form
|
||||||
|
x-data="{
|
||||||
|
save_active: inventory_new_item_check_input(),
|
||||||
|
weight_error: !check_weight(),
|
||||||
|
}"
|
||||||
name="new-item"
|
name="new-item"
|
||||||
id="new-item"
|
id="new-item"
|
||||||
action="/inventory/item/"
|
action="/inventory/item/"
|
||||||
@@ -328,74 +470,14 @@ impl InventoryNewItemForm {
|
|||||||
span ."mdi" ."mdi-playlist-plus" ."text-2xl" ."mr-4" {}
|
span ."mdi" ."mdi-playlist-plus" ."text-2xl" ."mr-4" {}
|
||||||
p ."inline" ."text-xl" { "Add new item" }
|
p ."inline" ."text-xl" { "Add new item" }
|
||||||
}
|
}
|
||||||
div ."w-11/12" ."mx-auto" {
|
div ."w-11/12" ."mx-auto" ."flex" ."flex-col" ."gap-8" {
|
||||||
div ."pb-8" {
|
(InventoryNewItemFormName::build(None, false))
|
||||||
div ."flex" ."flex-row" ."justify-center" ."items-start"{
|
(InventoryNewItemFormWeight::build())
|
||||||
label for="name" .font-bold ."w-1/2" ."p-2" ."text-center" { "Name" }
|
(InventoryNewItemFormCategory::build(&state, categories))
|
||||||
span ."w-1/2" {
|
|
||||||
input type="text" id="new-item-name" name="new-item-name"
|
|
||||||
."block"
|
|
||||||
."w-full"
|
|
||||||
."p-2"
|
|
||||||
."bg-gray-50"
|
|
||||||
."border-2"
|
|
||||||
."rounded"
|
|
||||||
."focus:outline-none"
|
|
||||||
."focus:bg-white"
|
|
||||||
."focus:border-purple-500"
|
|
||||||
{
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
div ."flex" ."flex-row" ."justify-center" ."items-center" ."pb-8" {
|
|
||||||
label for="weight" .font-bold ."w-1/2" .text-center { "Weight" }
|
|
||||||
span ."w-1/2" {
|
|
||||||
input
|
|
||||||
type="text"
|
|
||||||
id="new-item-weight"
|
|
||||||
name="new-item-weight"
|
|
||||||
."block"
|
|
||||||
."w-full"
|
|
||||||
."p-2"
|
|
||||||
."bg-gray-50"
|
|
||||||
."border-2"
|
|
||||||
."border-gray-300"
|
|
||||||
."rounded"
|
|
||||||
."focus:outline-none"
|
|
||||||
."focus:bg-white"
|
|
||||||
."focus:border-purple-500"
|
|
||||||
{
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
div ."flex" ."flex-row" ."justify-center" ."items-center" ."pb-8" {
|
|
||||||
label for="item-category" .font-bold ."w-1/2" .text-center { "Category" }
|
|
||||||
span ."w-1/2" {
|
|
||||||
select
|
|
||||||
id="new-item-category-id"
|
|
||||||
name="new-item-category-id"
|
|
||||||
."block"
|
|
||||||
."w-full"
|
|
||||||
."p-2"
|
|
||||||
."bg-gray-50"
|
|
||||||
."border-2"
|
|
||||||
."border-gray-300"
|
|
||||||
."rounded"
|
|
||||||
."focus:outline-none"
|
|
||||||
."focus:bg-white"
|
|
||||||
."focus:border-purple-500"
|
|
||||||
autocomplete="off" // https://stackoverflow.com/a/10096033
|
|
||||||
{
|
|
||||||
@for category in categories {
|
|
||||||
option value=(category.id) selected[state.active_category_id.map_or(false, |id| id == category.id)] {
|
|
||||||
(category.name)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
input type="submit" value="Add"
|
input type="submit" value="Add"
|
||||||
|
x-bind:disabled="!save_active"
|
||||||
|
."enabled:cursor-pointer"
|
||||||
|
."disabled:opacity-50"
|
||||||
."py-2"
|
."py-2"
|
||||||
."border-2"
|
."border-2"
|
||||||
."rounded"
|
."rounded"
|
||||||
@@ -415,6 +497,7 @@ impl InventoryNewCategoryForm {
|
|||||||
pub fn build() -> Markup {
|
pub fn build() -> Markup {
|
||||||
html!(
|
html!(
|
||||||
form
|
form
|
||||||
|
x-data="{ save_active: document.getElementById('new-category-name').value.length != 0 }"
|
||||||
name="new-category"
|
name="new-category"
|
||||||
id="new-category"
|
id="new-category"
|
||||||
action="/inventory/category/"
|
action="/inventory/category/"
|
||||||
@@ -431,11 +514,13 @@ impl InventoryNewCategoryForm {
|
|||||||
label for="name" .font-bold ."w-1/2" ."p-2" ."text-center" { "Name" }
|
label for="name" .font-bold ."w-1/2" ."p-2" ."text-center" { "Name" }
|
||||||
span ."w-1/2" {
|
span ."w-1/2" {
|
||||||
input type="text" id="new-category-name" name="new-category-name"
|
input type="text" id="new-category-name" name="new-category-name"
|
||||||
|
x-on:input="(e) => {save_active = e.target.value.length != 0 }"
|
||||||
."block"
|
."block"
|
||||||
."w-full"
|
."w-full"
|
||||||
."p-2"
|
."p-2"
|
||||||
."bg-gray-50"
|
."bg-gray-50"
|
||||||
."border-2"
|
."border-2"
|
||||||
|
."border-gray-300"
|
||||||
."rounded"
|
."rounded"
|
||||||
."focus:outline-none"
|
."focus:outline-none"
|
||||||
."focus:bg-white"
|
."focus:bg-white"
|
||||||
@@ -446,6 +531,9 @@ impl InventoryNewCategoryForm {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
input type="submit" value="Add"
|
input type="submit" value="Add"
|
||||||
|
x-bind:disabled="!save_active"
|
||||||
|
."enabled:cursor-pointer"
|
||||||
|
."disabled:opacity-50"
|
||||||
."py-2"
|
."py-2"
|
||||||
."border-2"
|
."border-2"
|
||||||
."rounded"
|
."rounded"
|
||||||
|
|||||||
@@ -24,6 +24,7 @@ impl Root {
|
|||||||
head {
|
head {
|
||||||
title { "Packager" }
|
title { "Packager" }
|
||||||
script src="https://unpkg.com/htmx.org@1.9.2" {}
|
script src="https://unpkg.com/htmx.org@1.9.2" {}
|
||||||
|
script src="https://unpkg.com/alpinejs@3.12.1" defer {}
|
||||||
script src="https://cdn.tailwindcss.com" {}
|
script src="https://cdn.tailwindcss.com" {}
|
||||||
script src="https://unpkg.com/alpinejs@3.x.x/dist/cdn.js" defer {}
|
script src="https://unpkg.com/alpinejs@3.x.x/dist/cdn.js" defer {}
|
||||||
link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/@mdi/font@6.9.96/css/materialdesignicons.min.css";
|
link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/@mdi/font@6.9.96/css/materialdesignicons.min.css";
|
||||||
|
|||||||
@@ -9,6 +9,9 @@ use serde_variant::to_variant_name;
|
|||||||
use crate::ClientState;
|
use crate::ClientState;
|
||||||
pub struct TripManager;
|
pub struct TripManager;
|
||||||
|
|
||||||
|
pub mod types;
|
||||||
|
pub use types::*;
|
||||||
|
|
||||||
impl TripManager {
|
impl TripManager {
|
||||||
pub fn build(trips: Vec<models::Trip>) -> Markup {
|
pub fn build(trips: Vec<models::Trip>) -> Markup {
|
||||||
html!(
|
html!(
|
||||||
@@ -557,7 +560,7 @@ impl TripInfo {
|
|||||||
."gap-1"
|
."gap-1"
|
||||||
{
|
{
|
||||||
span { (triptype.name) }
|
span { (triptype.name) }
|
||||||
span ."mdi" ."mdi-delete" ."text-sm" {}
|
span ."mdi" ."mdi-close" ."text-sm" {}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -584,6 +587,7 @@ impl TripInfo {
|
|||||||
."flex-column"
|
."flex-column"
|
||||||
."items-center"
|
."items-center"
|
||||||
."hover:bg-green-200"
|
."hover:bg-green-200"
|
||||||
|
."hover:opacity-100"
|
||||||
."gap-1"
|
."gap-1"
|
||||||
."opacity-60"
|
."opacity-60"
|
||||||
{
|
{
|
||||||
@@ -623,7 +627,9 @@ pub struct TripComment;
|
|||||||
impl TripComment {
|
impl TripComment {
|
||||||
pub fn build(trip: &models::Trip) -> Markup {
|
pub fn build(trip: &models::Trip) -> Markup {
|
||||||
html!(
|
html!(
|
||||||
div {
|
div
|
||||||
|
x-data="{ save_active: false }"
|
||||||
|
{
|
||||||
h1 ."text-xl" ."mb-5" { "Comments" }
|
h1 ."text-xl" ."mb-5" { "Comments" }
|
||||||
|
|
||||||
form
|
form
|
||||||
@@ -636,6 +642,7 @@ impl TripComment {
|
|||||||
// https://stackoverflow.com/a/48460773
|
// https://stackoverflow.com/a/48460773
|
||||||
textarea
|
textarea
|
||||||
#"comment"
|
#"comment"
|
||||||
|
x-on:input="save_active=true"
|
||||||
."border" ."w-full" ."h-48"
|
."border" ."w-full" ."h-48"
|
||||||
name="new-comment"
|
name="new-comment"
|
||||||
form="edit-comment"
|
form="edit-comment"
|
||||||
@@ -647,11 +654,14 @@ impl TripComment {
|
|||||||
button
|
button
|
||||||
type="submit"
|
type="submit"
|
||||||
form="edit-comment"
|
form="edit-comment"
|
||||||
|
x-bind:disabled="!save_active"
|
||||||
|
."enabled:bg-green-200"
|
||||||
|
."enabled:hover:bg-green-400"
|
||||||
|
."enabled:cursor-pointer"
|
||||||
|
."disabled:opacity-50"
|
||||||
|
."disabled:bg-gray-300"
|
||||||
."mt-2"
|
."mt-2"
|
||||||
."border"
|
."border"
|
||||||
."bg-green-200"
|
|
||||||
."hover:bg-green-400"
|
|
||||||
."cursor-pointer"
|
|
||||||
."flex"
|
."flex"
|
||||||
."flex-column"
|
."flex-column"
|
||||||
."p-2"
|
."p-2"
|
||||||
163
rust/src/components/trip/types.rs
Normal file
163
rust/src/components/trip/types.rs
Normal file
@@ -0,0 +1,163 @@
|
|||||||
|
use crate::models;
|
||||||
|
use crate::ClientState;
|
||||||
|
use maud::{html, Markup};
|
||||||
|
|
||||||
|
pub struct TypeList;
|
||||||
|
|
||||||
|
impl TypeList {
|
||||||
|
pub fn build(state: &ClientState, trip_types: Vec<models::TripsType>) -> Markup {
|
||||||
|
html!(
|
||||||
|
div ."p-8" ."flex" ."flex-col" ."gap-8" {
|
||||||
|
h1 ."text-2xl" {"Trip Types"}
|
||||||
|
|
||||||
|
ul
|
||||||
|
."flex"
|
||||||
|
."flex-col"
|
||||||
|
."items-stretch"
|
||||||
|
."border-t"
|
||||||
|
."border-l"
|
||||||
|
."h-full"
|
||||||
|
{
|
||||||
|
@for trip_type in trip_types {
|
||||||
|
li
|
||||||
|
."border-b"
|
||||||
|
."border-r"
|
||||||
|
."flex"
|
||||||
|
."flex-row"
|
||||||
|
."justify-between"
|
||||||
|
."items-stretch"
|
||||||
|
{
|
||||||
|
@if state.trip_type_edit.map_or(false, |id| id == trip_type.id) {
|
||||||
|
form
|
||||||
|
."hidden"
|
||||||
|
id="edit-trip-type"
|
||||||
|
action={ (trip_type.id) "/edit/name/submit" }
|
||||||
|
target="_self"
|
||||||
|
method="post"
|
||||||
|
{}
|
||||||
|
div
|
||||||
|
."bg-blue-200"
|
||||||
|
."p-2"
|
||||||
|
."grow"
|
||||||
|
{
|
||||||
|
input
|
||||||
|
."bg-blue-100"
|
||||||
|
."hover:bg-white"
|
||||||
|
."w-full"
|
||||||
|
type="text"
|
||||||
|
name="new-value"
|
||||||
|
form="edit-trip-type"
|
||||||
|
value=(trip_type.name)
|
||||||
|
;
|
||||||
|
}
|
||||||
|
div
|
||||||
|
."flex"
|
||||||
|
."flex-row"
|
||||||
|
{
|
||||||
|
a
|
||||||
|
href="."
|
||||||
|
."bg-red-200"
|
||||||
|
."hover:bg-red-300"
|
||||||
|
."w-8"
|
||||||
|
."flex"
|
||||||
|
{
|
||||||
|
span
|
||||||
|
."mdi"
|
||||||
|
."mdi-cancel"
|
||||||
|
."text-xl"
|
||||||
|
."m-auto"
|
||||||
|
;
|
||||||
|
}
|
||||||
|
button
|
||||||
|
type="submit"
|
||||||
|
form="edit-trip-type"
|
||||||
|
."bg-green-200"
|
||||||
|
."hover:bg-green-300"
|
||||||
|
."w-8"
|
||||||
|
{
|
||||||
|
span
|
||||||
|
."mdi"
|
||||||
|
."mdi-content-save"
|
||||||
|
."text-xl"
|
||||||
|
;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} @else {
|
||||||
|
span
|
||||||
|
."p-2"
|
||||||
|
{
|
||||||
|
(trip_type.name)
|
||||||
|
}
|
||||||
|
|
||||||
|
div
|
||||||
|
."bg-blue-100"
|
||||||
|
."hover:bg-blue-200"
|
||||||
|
."p-0"
|
||||||
|
."w-8"
|
||||||
|
{
|
||||||
|
a
|
||||||
|
.flex
|
||||||
|
."w-full"
|
||||||
|
."h-full"
|
||||||
|
href={ "?edit=" (trip_type.id) }
|
||||||
|
{
|
||||||
|
span
|
||||||
|
."m-auto"
|
||||||
|
."mdi"
|
||||||
|
."mdi-pencil"
|
||||||
|
."text-xl";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
form
|
||||||
|
name="new-trip-type"
|
||||||
|
action="/trips/types/"
|
||||||
|
target="_self"
|
||||||
|
method="post"
|
||||||
|
."mt-8" ."p-5" ."border-2" ."border-gray-200"
|
||||||
|
{
|
||||||
|
div ."mb-5" ."flex" ."flex-row" {
|
||||||
|
span ."mdi" ."mdi-playlist-plus" ."text-2xl" ."mr-4" {}
|
||||||
|
p ."inline" ."text-xl" { "Add new trip type" }
|
||||||
|
}
|
||||||
|
div ."w-11/12" ."m-auto" {
|
||||||
|
div ."mx-auto" ."pb-8" {
|
||||||
|
div ."flex" ."flex-row" ."justify-center" {
|
||||||
|
label for="new-trip-type-name" ."font-bold" ."w-1/2" ."p-2" ."text-center" { "Name" }
|
||||||
|
span ."w-1/2" {
|
||||||
|
input
|
||||||
|
type="text"
|
||||||
|
id="new-trip-type-name"
|
||||||
|
name="new-trip-type-name"
|
||||||
|
."block"
|
||||||
|
."w-full"
|
||||||
|
."p-2"
|
||||||
|
."bg-gray-50"
|
||||||
|
."border-2"
|
||||||
|
."rounded"
|
||||||
|
."focus:outline-none"
|
||||||
|
."focus:bg-white"
|
||||||
|
{}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
input
|
||||||
|
type="submit"
|
||||||
|
value="Add"
|
||||||
|
."py-2"
|
||||||
|
."border-2"
|
||||||
|
."rounded"
|
||||||
|
."border-gray-300"
|
||||||
|
."mx-auto"
|
||||||
|
."w-full"
|
||||||
|
{}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
254
rust/src/main.rs
254
rust/src/main.rs
@@ -47,6 +47,7 @@ pub struct ClientState {
|
|||||||
pub active_category_id: Option<Uuid>,
|
pub active_category_id: Option<Uuid>,
|
||||||
pub edit_item: Option<Uuid>,
|
pub edit_item: Option<Uuid>,
|
||||||
pub trip_edit_attribute: Option<TripAttribute>,
|
pub trip_edit_attribute: Option<TripAttribute>,
|
||||||
|
pub trip_type_edit: Option<Uuid>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl ClientState {
|
impl ClientState {
|
||||||
@@ -55,6 +56,7 @@ impl ClientState {
|
|||||||
active_category_id: None,
|
active_category_id: None,
|
||||||
edit_item: None,
|
edit_item: None,
|
||||||
trip_edit_attribute: None,
|
trip_edit_attribute: None,
|
||||||
|
trip_type_edit: None,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -102,6 +104,11 @@ async fn main() -> Result<(), sqlx::Error> {
|
|||||||
.route("/assets/luggage.svg", get(icon_handler))
|
.route("/assets/luggage.svg", get(icon_handler))
|
||||||
.route("/", get(root))
|
.route("/", get(root))
|
||||||
.route("/trips/", get(trips))
|
.route("/trips/", get(trips))
|
||||||
|
.route("/trips/types/", get(trips_types).post(trip_type_create))
|
||||||
|
.route(
|
||||||
|
"/trips/types/:id/edit/name/submit",
|
||||||
|
post(trips_types_edit_name),
|
||||||
|
)
|
||||||
.route("/trip/", post(trip_create))
|
.route("/trip/", post(trip_create))
|
||||||
.route("/trip/:id/", get(trip))
|
.route("/trip/:id/", get(trip))
|
||||||
.route("/trip/:id/comment/submit", post(trip_comment_set))
|
.route("/trip/:id/comment/submit", post(trip_comment_set))
|
||||||
@@ -119,6 +126,10 @@ async fn main() -> Result<(), sqlx::Error> {
|
|||||||
.route("/inventory/", get(inventory_inactive))
|
.route("/inventory/", get(inventory_inactive))
|
||||||
.route("/inventory/category/", post(inventory_category_create))
|
.route("/inventory/category/", post(inventory_category_create))
|
||||||
.route("/inventory/item/", post(inventory_item_create))
|
.route("/inventory/item/", post(inventory_item_create))
|
||||||
|
.route(
|
||||||
|
"/inventory/item/name/validate",
|
||||||
|
post(inventory_item_validate_name),
|
||||||
|
)
|
||||||
.route("/inventory/category/:id/", get(inventory_active))
|
.route("/inventory/category/:id/", get(inventory_active))
|
||||||
.route("/inventory/item/:id/delete", get(inventory_item_delete))
|
.route("/inventory/item/:id/delete", get(inventory_item_delete))
|
||||||
.route("/inventory/item/:id/edit", post(inventory_item_edit))
|
.route("/inventory/item/:id/edit", post(inventory_item_edit))
|
||||||
@@ -250,10 +261,62 @@ struct NewItem {
|
|||||||
category_id: Uuid,
|
category_id: Uuid,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[derive(Deserialize)]
|
||||||
|
struct NewItemName {
|
||||||
|
#[serde(rename = "new-item-name")]
|
||||||
|
name: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn inventory_item_validate_name(
|
||||||
|
State(state): State<AppState>,
|
||||||
|
Form(new_item): Form<NewItemName>,
|
||||||
|
) -> Result<(StatusCode, Markup), (StatusCode, Markup)> {
|
||||||
|
let results = query!(
|
||||||
|
"SELECT id
|
||||||
|
FROM inventory_items
|
||||||
|
WHERE name = ?",
|
||||||
|
new_item.name,
|
||||||
|
)
|
||||||
|
.fetch(&state.database_pool)
|
||||||
|
.map_ok(|_| Ok(()))
|
||||||
|
.try_collect::<Vec<Result<(), models::Error>>>()
|
||||||
|
.await
|
||||||
|
// we have two error handling lines here. these are distinct errors
|
||||||
|
// this one is the SQL error that may arise during the query
|
||||||
|
.map_err(|e| {
|
||||||
|
(
|
||||||
|
StatusCode::INTERNAL_SERVER_ERROR,
|
||||||
|
ErrorPage::build(&e.to_string()),
|
||||||
|
)
|
||||||
|
})?
|
||||||
|
.into_iter()
|
||||||
|
.collect::<Result<Vec<()>, models::Error>>()
|
||||||
|
// and this one is the model mapping error that may arise e.g. during
|
||||||
|
// reading of the rows
|
||||||
|
.map_err(|e| {
|
||||||
|
(
|
||||||
|
StatusCode::INTERNAL_SERVER_ERROR,
|
||||||
|
ErrorPage::build(&e.to_string()),
|
||||||
|
)
|
||||||
|
})?;
|
||||||
|
|
||||||
|
Ok((
|
||||||
|
StatusCode::OK,
|
||||||
|
InventoryNewItemFormName::build(Some(&new_item.name), !results.is_empty()),
|
||||||
|
))
|
||||||
|
}
|
||||||
|
|
||||||
async fn inventory_item_create(
|
async fn inventory_item_create(
|
||||||
State(state): State<AppState>,
|
State(state): State<AppState>,
|
||||||
Form(new_item): Form<NewItem>,
|
Form(new_item): Form<NewItem>,
|
||||||
) -> Result<Redirect, (StatusCode, String)> {
|
) -> Result<Redirect, (StatusCode, String)> {
|
||||||
|
if new_item.name.len() == 0 {
|
||||||
|
return Err((
|
||||||
|
StatusCode::UNPROCESSABLE_ENTITY,
|
||||||
|
"name cannot be empty".to_string(),
|
||||||
|
));
|
||||||
|
}
|
||||||
|
|
||||||
let id = Uuid::new_v4();
|
let id = Uuid::new_v4();
|
||||||
let id_param = id.to_string();
|
let id_param = id.to_string();
|
||||||
let name = &new_item.name;
|
let name = &new_item.name;
|
||||||
@@ -411,6 +474,13 @@ async fn inventory_item_edit(
|
|||||||
Path(id): Path<Uuid>,
|
Path(id): Path<Uuid>,
|
||||||
Form(edit_item): Form<EditItem>,
|
Form(edit_item): Form<EditItem>,
|
||||||
) -> Result<Redirect, (StatusCode, Markup)> {
|
) -> Result<Redirect, (StatusCode, Markup)> {
|
||||||
|
if edit_item.name.len() == 0 {
|
||||||
|
return Err((
|
||||||
|
StatusCode::UNPROCESSABLE_ENTITY,
|
||||||
|
ErrorPage::build("name cannot be empty"),
|
||||||
|
));
|
||||||
|
}
|
||||||
|
|
||||||
let id = Item::update(
|
let id = Item::update(
|
||||||
&state.database_pool,
|
&state.database_pool,
|
||||||
id,
|
id,
|
||||||
@@ -469,6 +539,13 @@ async fn trip_create(
|
|||||||
State(state): State<AppState>,
|
State(state): State<AppState>,
|
||||||
Form(new_trip): Form<NewTrip>,
|
Form(new_trip): Form<NewTrip>,
|
||||||
) -> Result<Redirect, (StatusCode, String)> {
|
) -> Result<Redirect, (StatusCode, String)> {
|
||||||
|
if new_trip.name.len() == 0 {
|
||||||
|
return Err((
|
||||||
|
StatusCode::UNPROCESSABLE_ENTITY,
|
||||||
|
"name cannot be empty".to_string(),
|
||||||
|
));
|
||||||
|
}
|
||||||
|
|
||||||
let id = Uuid::new_v4();
|
let id = Uuid::new_v4();
|
||||||
let id_param = id.to_string();
|
let id_param = id.to_string();
|
||||||
let date_start = new_trip
|
let date_start = new_trip
|
||||||
@@ -532,8 +609,6 @@ async fn trip_create(
|
|||||||
async fn trips(
|
async fn trips(
|
||||||
State(state): State<AppState>,
|
State(state): State<AppState>,
|
||||||
) -> Result<(StatusCode, Markup), (StatusCode, Markup)> {
|
) -> Result<(StatusCode, Markup), (StatusCode, Markup)> {
|
||||||
tracing::info!("receiving trips");
|
|
||||||
|
|
||||||
let trips: Vec<models::Trip> = query_as!(
|
let trips: Vec<models::Trip> = query_as!(
|
||||||
DbTripRow,
|
DbTripRow,
|
||||||
"SELECT
|
"SELECT
|
||||||
@@ -571,8 +646,6 @@ async fn trips(
|
|||||||
)
|
)
|
||||||
})?;
|
})?;
|
||||||
|
|
||||||
tracing::info!("received trips");
|
|
||||||
|
|
||||||
Ok((
|
Ok((
|
||||||
StatusCode::OK,
|
StatusCode::OK,
|
||||||
Root::build(TripManager::build(trips), &TopLevelPage::Trips),
|
Root::build(TripManager::build(trips), &TopLevelPage::Trips),
|
||||||
@@ -811,6 +884,14 @@ async fn trip_edit_attribute(
|
|||||||
Path((trip_id, attribute)): Path<(Uuid, TripAttribute)>,
|
Path((trip_id, attribute)): Path<(Uuid, TripAttribute)>,
|
||||||
Form(trip_update): Form<TripUpdate>,
|
Form(trip_update): Form<TripUpdate>,
|
||||||
) -> Result<Redirect, (StatusCode, Markup)> {
|
) -> Result<Redirect, (StatusCode, Markup)> {
|
||||||
|
if let TripAttribute::Name = attribute {
|
||||||
|
if trip_update.new_value.len() == 0 {
|
||||||
|
return Err((
|
||||||
|
StatusCode::UNPROCESSABLE_ENTITY,
|
||||||
|
ErrorPage::build("name cannot be empty"),
|
||||||
|
));
|
||||||
|
}
|
||||||
|
}
|
||||||
let result = query(&format!(
|
let result = query(&format!(
|
||||||
"UPDATE trips
|
"UPDATE trips
|
||||||
SET {attribute} = ?
|
SET {attribute} = ?
|
||||||
@@ -980,6 +1061,13 @@ async fn inventory_category_create(
|
|||||||
State(state): State<AppState>,
|
State(state): State<AppState>,
|
||||||
Form(new_category): Form<NewCategory>,
|
Form(new_category): Form<NewCategory>,
|
||||||
) -> Result<Redirect, (StatusCode, Markup)> {
|
) -> Result<Redirect, (StatusCode, Markup)> {
|
||||||
|
if new_category.name.len() == 0 {
|
||||||
|
return Err((
|
||||||
|
StatusCode::UNPROCESSABLE_ENTITY,
|
||||||
|
ErrorPage::build("name cannot be empty"),
|
||||||
|
));
|
||||||
|
}
|
||||||
|
|
||||||
let id = Uuid::new_v4();
|
let id = Uuid::new_v4();
|
||||||
let id_param = id.to_string();
|
let id_param = id.to_string();
|
||||||
query!(
|
query!(
|
||||||
@@ -1059,3 +1147,161 @@ async fn trip_state_set(
|
|||||||
Ok(Redirect::to(&format!("/trip/{id}/", id = trip_id)))
|
Ok(Redirect::to(&format!("/trip/{id}/", id = trip_id)))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Deserialize)]
|
||||||
|
struct TripTypeQuery {
|
||||||
|
edit: Option<Uuid>,
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn trips_types(
|
||||||
|
State(mut state): State<AppState>,
|
||||||
|
Query(trip_type_query): Query<TripTypeQuery>,
|
||||||
|
) -> Result<(StatusCode, Markup), (StatusCode, Markup)> {
|
||||||
|
state.client_state.trip_type_edit = trip_type_query.edit;
|
||||||
|
|
||||||
|
let trip_types: Vec<models::TripsType> = query_as!(
|
||||||
|
DbTripsTypesRow,
|
||||||
|
"SELECT
|
||||||
|
id,
|
||||||
|
name
|
||||||
|
FROM trips_types",
|
||||||
|
)
|
||||||
|
.fetch(&state.database_pool)
|
||||||
|
.map_ok(|row| row.try_into())
|
||||||
|
.try_collect::<Vec<Result<models::TripsType, models::Error>>>()
|
||||||
|
.await
|
||||||
|
// we have two error handling lines here. these are distinct errors
|
||||||
|
// this one is the SQL error that may arise during the query
|
||||||
|
.map_err(|e| {
|
||||||
|
(
|
||||||
|
StatusCode::INTERNAL_SERVER_ERROR,
|
||||||
|
ErrorPage::build(&e.to_string()),
|
||||||
|
)
|
||||||
|
})?
|
||||||
|
.into_iter()
|
||||||
|
.collect::<Result<Vec<models::TripsType>, models::Error>>()
|
||||||
|
// and this one is the model mapping error that may arise e.g. during
|
||||||
|
// reading of the rows
|
||||||
|
.map_err(|e| {
|
||||||
|
(
|
||||||
|
StatusCode::INTERNAL_SERVER_ERROR,
|
||||||
|
ErrorPage::build(&e.to_string()),
|
||||||
|
)
|
||||||
|
})?;
|
||||||
|
|
||||||
|
Ok((
|
||||||
|
StatusCode::OK,
|
||||||
|
Root::build(
|
||||||
|
components::trip::TypeList::build(&state.client_state, trip_types),
|
||||||
|
&TopLevelPage::Trips,
|
||||||
|
),
|
||||||
|
))
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Deserialize)]
|
||||||
|
struct NewTripType {
|
||||||
|
#[serde(rename = "new-trip-type-name")]
|
||||||
|
name: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn trip_type_create(
|
||||||
|
State(state): State<AppState>,
|
||||||
|
Form(new_trip_type): Form<NewTripType>,
|
||||||
|
) -> Result<Redirect, (StatusCode, String)> {
|
||||||
|
if new_trip_type.name.len() == 0 {
|
||||||
|
return Err((
|
||||||
|
StatusCode::UNPROCESSABLE_ENTITY,
|
||||||
|
"name cannot be empty".to_string(),
|
||||||
|
));
|
||||||
|
}
|
||||||
|
|
||||||
|
let id = Uuid::new_v4();
|
||||||
|
let id_param = id.to_string();
|
||||||
|
query!(
|
||||||
|
"INSERT INTO trips_types
|
||||||
|
(id, name)
|
||||||
|
VALUES
|
||||||
|
(?, ?)",
|
||||||
|
id_param,
|
||||||
|
new_trip_type.name,
|
||||||
|
)
|
||||||
|
.execute(&state.database_pool)
|
||||||
|
.await
|
||||||
|
.map_err(|e| match e {
|
||||||
|
sqlx::Error::Database(ref error) => {
|
||||||
|
let sqlite_error = error.downcast_ref::<SqliteError>();
|
||||||
|
if let Some(code) = sqlite_error.code() {
|
||||||
|
match &*code {
|
||||||
|
"2067" => {
|
||||||
|
// SQLITE_CONSTRAINT_UNIQUE
|
||||||
|
(
|
||||||
|
StatusCode::BAD_REQUEST,
|
||||||
|
format!(
|
||||||
|
"trip type with name \"{name}\" already exists",
|
||||||
|
name = new_trip_type.name,
|
||||||
|
),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
_ => (
|
||||||
|
StatusCode::INTERNAL_SERVER_ERROR,
|
||||||
|
format!("got error with unknown code: {}", sqlite_error.to_string()),
|
||||||
|
),
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
(
|
||||||
|
StatusCode::INTERNAL_SERVER_ERROR,
|
||||||
|
format!("got error without code: {}", sqlite_error.to_string()),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
_ => (
|
||||||
|
StatusCode::INTERNAL_SERVER_ERROR,
|
||||||
|
format!("got unknown error: {}", e.to_string()),
|
||||||
|
),
|
||||||
|
})?;
|
||||||
|
|
||||||
|
Ok(Redirect::to("/trips/types/"))
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Deserialize)]
|
||||||
|
struct TripTypeUpdate {
|
||||||
|
#[serde(rename = "new-value")]
|
||||||
|
new_value: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn trips_types_edit_name(
|
||||||
|
State(state): State<AppState>,
|
||||||
|
Path(trip_type_id): Path<Uuid>,
|
||||||
|
Form(trip_update): Form<TripTypeUpdate>,
|
||||||
|
) -> Result<Redirect, (StatusCode, Markup)> {
|
||||||
|
if trip_update.new_value.len() == 0 {
|
||||||
|
return Err((
|
||||||
|
StatusCode::UNPROCESSABLE_ENTITY,
|
||||||
|
ErrorPage::build("name cannot be empty"),
|
||||||
|
));
|
||||||
|
}
|
||||||
|
|
||||||
|
let id_param = trip_type_id.to_string();
|
||||||
|
let result = query!(
|
||||||
|
"UPDATE trips_types
|
||||||
|
SET name = ?
|
||||||
|
WHERE id = ?",
|
||||||
|
trip_update.new_value,
|
||||||
|
id_param,
|
||||||
|
)
|
||||||
|
.execute(&state.database_pool)
|
||||||
|
.await
|
||||||
|
.map_err(|e| (StatusCode::BAD_REQUEST, ErrorPage::build(&e.to_string())))?;
|
||||||
|
|
||||||
|
if result.rows_affected() == 0 {
|
||||||
|
Err((
|
||||||
|
StatusCode::NOT_FOUND,
|
||||||
|
ErrorPage::build(&format!(
|
||||||
|
"tript type with id {id} not found",
|
||||||
|
id = trip_type_id
|
||||||
|
)),
|
||||||
|
))
|
||||||
|
} else {
|
||||||
|
Ok(Redirect::to("/trips/types/"))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -288,69 +288,6 @@ pub enum TripAttribute {
|
|||||||
TempMax,
|
TempMax,
|
||||||
}
|
}
|
||||||
|
|
||||||
// impl std::convert::Into<&'static str> for TripAttribute {
|
|
||||||
// fn into(self) -> &'static str {
|
|
||||||
// match self {
|
|
||||||
// Self::DateStart => "date_start",
|
|
||||||
// Self::DateEnd => "date_end",
|
|
||||||
// Self::Location => "location",
|
|
||||||
// Self::TempMin => "temp_min",
|
|
||||||
// Self::TempMax => "temp_max",
|
|
||||||
// }
|
|
||||||
// }
|
|
||||||
// }
|
|
||||||
|
|
||||||
// impl std::convert::TryFrom<&str> for TripAttribute {
|
|
||||||
// type Error = Error;
|
|
||||||
|
|
||||||
// fn try_from(value: &str) -> Result<Self, Error> {
|
|
||||||
// Ok(match value {
|
|
||||||
// "date_start" => Self::DateStart,
|
|
||||||
// "date_end" => Self::DateEnd,
|
|
||||||
// "location" => Self::Location,
|
|
||||||
// "temp_min" => Self::TempMin,
|
|
||||||
// "temp_max" => Self::TempMax,
|
|
||||||
// _ => {
|
|
||||||
// return Err(Error::UnknownAttributeValue {
|
|
||||||
// attribute: value.to_string(),
|
|
||||||
// })
|
|
||||||
// }
|
|
||||||
// })
|
|
||||||
// }
|
|
||||||
// }
|
|
||||||
|
|
||||||
// impl TryFrom<SqliteRow> for Trip {
|
|
||||||
// type Error = Error;
|
|
||||||
|
|
||||||
// fn try_from(row: SqliteRow) -> Result<Self, Self::Error> {
|
|
||||||
// let name: &str = row.try_get("name")?;
|
|
||||||
// let id: &str = row.try_get("id")?;
|
|
||||||
// let date_start: time::Date = row.try_get("date_start")?;
|
|
||||||
// let date_end: time::Date = row.try_get("date_end")?;
|
|
||||||
// let state: TripState = row.try_get("state")?;
|
|
||||||
// let location = row.try_get("location")?;
|
|
||||||
// let temp_min = row.try_get("temp_min")?;
|
|
||||||
// let temp_max = row.try_get("temp_max")?;
|
|
||||||
// let comment = row.try_get("comment")?;
|
|
||||||
|
|
||||||
// let id: Uuid = Uuid::try_parse(id)?;
|
|
||||||
|
|
||||||
// Ok(Trip {
|
|
||||||
// id,
|
|
||||||
// name: name.to_string(),
|
|
||||||
// date_start,
|
|
||||||
// date_end,
|
|
||||||
// state,
|
|
||||||
// location,
|
|
||||||
// temp_min,
|
|
||||||
// temp_max,
|
|
||||||
// comment,
|
|
||||||
// types: None,
|
|
||||||
// categories: None,
|
|
||||||
// })
|
|
||||||
// }
|
|
||||||
// }
|
|
||||||
|
|
||||||
impl<'a> Trip {
|
impl<'a> Trip {
|
||||||
pub fn types(&'a self) -> &Vec<TripType> {
|
pub fn types(&'a self) -> &Vec<TripType> {
|
||||||
self.types
|
self.types
|
||||||
@@ -614,18 +551,6 @@ pub struct TripType {
|
|||||||
pub active: bool,
|
pub active: bool,
|
||||||
}
|
}
|
||||||
|
|
||||||
// impl TryFrom<SqliteRow> for TripType {
|
|
||||||
// type Error = Error;
|
|
||||||
|
|
||||||
// fn try_from(row: SqliteRow) -> Result<Self, Self::Error> {
|
|
||||||
// let id: Uuid = Uuid::try_parse(row.try_get("id")?)?;
|
|
||||||
// let name: String = row.try_get::<&str, _>("name")?.to_string();
|
|
||||||
// let active: bool = row.try_get("active")?;
|
|
||||||
|
|
||||||
// Ok(Self { id, name, active })
|
|
||||||
// }
|
|
||||||
// }
|
|
||||||
|
|
||||||
pub struct DbCategoryRow {
|
pub struct DbCategoryRow {
|
||||||
pub id: String,
|
pub id: String,
|
||||||
pub name: String,
|
pub name: String,
|
||||||
@@ -653,23 +578,6 @@ impl TryFrom<DbCategoryRow> for Category {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// impl TryFrom<SqliteRow> for Category {
|
|
||||||
// type Error = Error;
|
|
||||||
|
|
||||||
// fn try_from(row: SqliteRow) -> Result<Self, Self::Error> {
|
|
||||||
// let name: &str = row.try_get("name")?;
|
|
||||||
// let description: &str = row.try_get("description")?;
|
|
||||||
// let id: Uuid = Uuid::try_parse(row.try_get("id")?)?;
|
|
||||||
|
|
||||||
// Ok(Category {
|
|
||||||
// id,
|
|
||||||
// name: name.to_string(),
|
|
||||||
// description: description.to_string(),
|
|
||||||
// items: None,
|
|
||||||
// })
|
|
||||||
// }
|
|
||||||
// }
|
|
||||||
|
|
||||||
pub struct DbInventoryItemsRow {
|
pub struct DbInventoryItemsRow {
|
||||||
id: String,
|
id: String,
|
||||||
name: String,
|
name: String,
|
||||||
@@ -741,26 +649,6 @@ impl TryFrom<DbInventoryItemsRow> for Item {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// impl TryFrom<SqliteRow> for Item {
|
|
||||||
// type Error = Error;
|
|
||||||
|
|
||||||
// fn try_from(row: SqliteRow) -> Result<Self, Self::Error> {
|
|
||||||
// let name: &str = row.try_get("name")?;
|
|
||||||
// let description: &str = row.try_get("description")?;
|
|
||||||
// let weight: i64 = row.try_get("weight")?;
|
|
||||||
// let id: Uuid = Uuid::try_parse(row.try_get("id")?)?;
|
|
||||||
// let category_id: Uuid = Uuid::try_parse(row.try_get("category_id")?)?;
|
|
||||||
|
|
||||||
// Ok(Item {
|
|
||||||
// id,
|
|
||||||
// name: name.to_string(),
|
|
||||||
// weight,
|
|
||||||
// description: description.to_string(),
|
|
||||||
// category_id,
|
|
||||||
// })
|
|
||||||
// }
|
|
||||||
// }
|
|
||||||
|
|
||||||
impl Item {
|
impl Item {
|
||||||
pub async fn find(pool: &sqlx::Pool<sqlx::Sqlite>, id: Uuid) -> Result<Option<Item>, Error> {
|
pub async fn find(pool: &sqlx::Pool<sqlx::Sqlite>, id: Uuid) -> Result<Option<Item>, Error> {
|
||||||
let id_param = id.to_string();
|
let id_param = id.to_string();
|
||||||
@@ -820,3 +708,30 @@ impl Item {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub struct DbTripsTypesRow {
|
||||||
|
pub id: String,
|
||||||
|
pub name: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq)]
|
||||||
|
pub enum TripTypeAttribute {
|
||||||
|
#[serde(rename = "name")]
|
||||||
|
Name,
|
||||||
|
}
|
||||||
|
|
||||||
|
pub struct TripsType {
|
||||||
|
pub id: Uuid,
|
||||||
|
pub name: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl TryFrom<DbTripsTypesRow> for TripsType {
|
||||||
|
type Error = Error;
|
||||||
|
|
||||||
|
fn try_from(row: DbTripsTypesRow) -> Result<Self, Self::Error> {
|
||||||
|
Ok(TripsType {
|
||||||
|
id: Uuid::try_parse(&row.id)?,
|
||||||
|
name: row.name,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user