add more stuff

This commit is contained in:
2023-08-29 21:34:00 +02:00
parent 894e59d862
commit aac192eb97
11 changed files with 872 additions and 214 deletions

View File

@@ -1,5 +1,6 @@
use maud::{html, Markup, PreEscaped};
use crate::models;
use crate::models::*;
use crate::ClientState;
use uuid::{uuid, Uuid};
@@ -221,7 +222,8 @@ impl InventoryItemList {
."m-auto"
."mdi"
."mdi-content-save"
."text-xl";
."text-xl"
{}
}
}
td
@@ -243,7 +245,8 @@ impl InventoryItemList {
."m-auto"
."mdi"
."mdi-cancel"
."text-xl";
."text-xl"
{}
}
}
}
@@ -282,7 +285,7 @@ impl InventoryItemList {
."w-full"
href=(format!("?edit_item={id}", id = item.id))
{
span ."m-auto" ."mdi" ."mdi-pencil" ."text-xl";
span ."m-auto" ."mdi" ."mdi-pencil" ."text-xl" {}
}
}
td
@@ -299,7 +302,7 @@ impl InventoryItemList {
."w-full"
href=(format!("/inventory/item/{id}/delete", id = item.id))
{
span ."m-auto" ."mdi" ."mdi-delete" ."text-xl";
span ."m-auto" ."mdi" ."mdi-delete" ."text-xl" {}
}
}
}
@@ -348,7 +351,7 @@ impl InventoryNewItemFormName {
."focus:bg-white"
."focus:border-purple-500"[!error]
value=[value]
;
{}
@if error {
div
."col-start-2"
@@ -549,3 +552,45 @@ impl InventoryNewCategoryForm {
)
}
}
pub struct InventoryItem;
impl InventoryItem {
pub fn build(_state: &ClientState, item: &models::InventoryItem) -> Markup {
html!(
div ."p-8" {
table
."table"
."table-auto"
."border-collapse"
."border-spacing-0"
."border"
."w-full"
{
tbody {
tr ."h-10" ."even:bg-gray-100" ."hover:bg-purple-100" ."h-full" {
td ."border" ."p-2" { "Name" }
td ."border" ."p-2" { (item.name) }
}
tr ."h-10" ."even:bg-gray-100" ."hover:bg-purple-100" ."h-full" {
td ."border" ."p-2" { "Description" }
td ."border" ."p-2" { (item.description.clone().unwrap_or("".to_string())) }
}
tr ."h-10" ."even:bg-gray-100" ."hover:bg-purple-100" ."h-full" {
td ."border" ."p-2" { "Weight" }
td ."border" ."p-2" { (item.weight.to_string()) }
}
tr ."h-10" ."even:bg-gray-100" ."hover:bg-purple-100" ."h-full" {
td ."border" ."p-2" { "Category" }
td ."border" ."p-2" { (item.category.name) }
}
}
}
@match item.product {
Some(ref product) => p { "this item is part of product" (product.name) },
None => p { "this item is not part of a product" },
}
}
)
}
}

View File

@@ -27,8 +27,8 @@ impl Root {
script src="https://unpkg.com/alpinejs@3.12.1" defer {}
script src="https://cdn.tailwindcss.com" {}
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="shortcut icon" type="image/svg+xml" href="/favicon.svg";
link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/@mdi/font@6.9.96/css/materialdesignicons.min.css" {}
link rel="shortcut icon" type="image/svg+xml" href="/favicon.svg" {}
script { (PreEscaped(include_str!(concat!(env!("CARGO_MANIFEST_DIR"),"/js/app.js")))) }
}
body
@@ -53,7 +53,7 @@ impl Root {
."items-center"
."gap-3"
{
img ."h-12" src="/assets/luggage.svg";
img ."h-12" src="/assets/luggage.svg" {}
a #home href="/" { "Packager" }
}
nav

View File

@@ -237,7 +237,7 @@ impl Trip {
name="new-value"
form="edit-trip"
value=(trip.name)
;
{}
a
href="."
."bg-red-200"
@@ -250,7 +250,7 @@ impl Trip {
."mdi-cancel"
."text-xl"
."m-auto"
;
{}
}
button
type="submit"
@@ -263,7 +263,7 @@ impl Trip {
."mdi"
."mdi-content-save"
."text-xl"
;
{}
}
}
}
@@ -277,7 +277,7 @@ impl Trip {
."mdi-pencil"
."text-xl"
."opacity-50"
;
{}
}
}
}
@@ -308,10 +308,9 @@ impl TripInfoRow {
name="edit-trip"
id="edit-trip"
action=(format!("edit/{key}/submit", key=(to_variant_name(&attribute_key).unwrap()) ))
htmx-push-url="true"
target="_self"
method="post"
;
{}
}
tr .h-full {
@if edit {
@@ -324,7 +323,7 @@ impl TripInfoRow {
name="new-value"
form="edit-trip"
value=(value.map_or(String::new(), |v| v.to_string()))
;
{}
}
}
td
@@ -347,7 +346,8 @@ impl TripInfoRow {
."m-auto"
."mdi"
."mdi-cancel"
."text-xl";
."text-xl"
{}
}
}
td
@@ -370,7 +370,8 @@ impl TripInfoRow {
."m-auto"
."mdi"
."mdi-content-save"
."text-xl";
."text-xl"
{}
}
}
} @else {
@@ -395,7 +396,8 @@ impl TripInfoRow {
."m-auto"
."mdi"
."mdi-pencil"
."text-xl";
."text-xl"
{}
}
}
}
@@ -493,7 +495,8 @@ impl TripInfo {
."m-auto"
."mdi"
."mdi-step-backward"
."text-xl";
."text-xl"
{}
}
}
}
@@ -525,7 +528,8 @@ impl TripInfo {
."m-auto"
."mdi"
."mdi-step-forward"
."text-xl";
."text-xl"
{}
}
}
}
@@ -658,7 +662,7 @@ impl TripComment {
action="comment/submit"
target="_self"
method="post"
;
{}
// https://stackoverflow.com/a/48460773
textarea
@@ -689,7 +693,7 @@ impl TripComment {
."gap-2"
."items-center"
{
span ."mdi" ."mdi-content-save" ."text-xl";
span ."mdi" ."mdi-content-save" ."text-xl" {}
span { "Save" }
}
}
@@ -735,6 +739,105 @@ impl TripItems {
}
}
pub struct TripCategoryListRow;
impl TripCategoryListRow {
pub fn build(
category: &TripCategory,
active: bool,
has_new_items: bool,
biggest_category_weight: i64,
) -> Markup {
html!(
tr
id={"category-" (category.category.id)}
."h-10"
."hover:bg-purple-100"
."m-3"
."h-full"
."outline"[active]
."outline-2"[active]
."outline-indigo-300"[active]
{
td
."border"
."m-0"
{
div
."p-0"
."flex"
."flex-row"
."items-center"
."group"
{
a
id="select-category"
href=(
format!(
"?category={id}",
id=category.category.id
)
)
."inline-block"
."p-2"
."m-0"
."w-full"
."grow"
."font-bold"[active]
{
(category.category.name.clone())
}
@if has_new_items {
div
."mr-2"
."flex"
."flex-row"
."items-center"
{
p
."hidden"
."group-hover:inline"
."text-sm"
."text-gray-500"
."grow"
{
"new items"
}
span
."mdi"
."mdi-exclamation-thick"
."text-xl"
."text-yellow-400"
."grow-0"
{}
}
}
}
}
td ."border" ."m-0" ."p-2" style="position:relative;" {
p {
(category.total_picked_weight().to_string())
}
div ."bg-blue-600" ."h-1.5"
style=(
format!(
"width: {width}%;position:absolute;left:0;bottom:0;right:0;",
width=(
(category.total_picked_weight() as f64)
/ (biggest_category_weight as f64)
* 100.0
)
)
) {}
}
}
)
}
}
pub struct TripCategoryList;
impl TripCategoryList {
@@ -771,90 +874,7 @@ impl TripCategoryList {
@for category in trip.categories() {
@let has_new_items = category.items.as_ref().unwrap().iter().any(|item| item.new);
@let active = state.active_category_id.map_or(false, |id| category.category.id == id);
tr
."h-10"
."hover:bg-purple-100"
."m-3"
."h-full"
."outline"[active]
."outline-2"[active]
."outline-indigo-300"[active]
{
td
."border"
."m-0"
{
div
."p-0"
."flex"
."flex-row"
."items-center"
."group"
{
a
id="select-category"
href=(
format!(
"?category={id}",
id=category.category.id
)
)
."inline-block"
."p-2"
."m-0"
."w-full"
."grow"
."font-bold"[active]
{
(category.category.name.clone())
}
@if has_new_items {
div
."mr-2"
."flex"
."flex-row"
."items-center"
{
p
."hidden"
."group-hover:inline"
."text-sm"
."text-gray-500"
."grow"
{
"new items"
}
span
."mdi"
."mdi-exclamation-thick"
."text-xl"
."text-yellow-400"
."grow-0"
;
}
}
}
}
td ."border" ."m-0" ."p-2" style="position:relative;" {
p {
(category.total_picked_weight().to_string())
}
div ."bg-blue-600" ."h-1.5"
style=(
format!(
"width: {width}%;position:absolute;left:0;bottom:0;right:0;",
width=(
(category.total_picked_weight() as f64)
/ (biggest_category_weight as f64)
* 100.0
)
)
) {}
}
}
(TripCategoryListRow::build(category, active, has_new_items, biggest_category_weight))
}
tr ."h-10" ."hover:bg-purple-200" ."bg-gray-300" ."font-bold" {
td ."border" ."p-0" ."m-0" {
@@ -894,7 +914,7 @@ impl TripItemList {
table
."table"
."table-auto"
.table-fixed
."table-fixed"
."border-collapse"
."border-spacing-0"
."border"
@@ -910,86 +930,7 @@ impl TripItemList {
}
tbody {
@for item in items {
tr ."h-10" ."even:bg-gray-100" ."hover:bg-purple-100" {
td {
a
href={
"/trip/" (trip.id)
"/items/" (item.item.id)
"/" (if item.picked { "unpick" } else { "pick" }) }
."inline-block"
."p-2"
."m-0"
."w-full"
."justify-center"
."content-center"
."flex"
{
input
type="checkbox"
checked[item.picked]
autocomplete="off"
;
}
}
td {
a
href={
"/trip/" (trip.id)
"/items/" (item.item.id)
"/" (if item.packed { "unpack" } else { "pack" }) }
."inline-block"
."p-2"
."m-0"
."w-full"
."justify-center"
."content-center"
."flex"
{
input
type="checkbox"
checked[item.packed]
autocomplete="off"
;
}
}
td ."border" ."p-0" {
div
."flex"
."flex-row"
."items-center"
{
a
."p-2" ."w-full" ."inline-block"
href=(
format!("/inventory/item/{id}/", id=item.item.id)
)
{
(item.item.name.clone())
}
@if item.new {
div ."mr-2" {
span
."mdi"
."mdi-exclamation-thick"
."text-xl"
."text-yellow-400"
."grow-0"
;
}
}
}
}
td ."border" ."p-2" style="position:relative;" {
p { (item.item.weight.to_string()) }
div ."bg-blue-600" ."h-1.5" style=(format!("
width: {width}%;
position:absolute;
left:0;
bottom:0;
right:0;", width=((item.item.weight as f64) / (biggest_item_weight as f64) * 100.0))) {}
}
}
(TripItemListRow::build(trip.id, item, biggest_item_weight))
}
}
}
@@ -997,3 +938,147 @@ impl TripItemList {
)
}
}
pub struct TripItemListRow;
impl TripItemListRow {
pub fn build(trip_id: Uuid, item: &models::TripItem, biggest_item_weight: i64) -> Markup {
html!(
tr ."h-10" {
td
."border"
."p-0"
{
a
href={
"/trip/" (trip_id)
"/items/" (item.item.id)
"/" (if item.picked { "unpick" } else { "pick" }) }
hx-post={
"/trip/" (trip_id)
"/items/" (item.item.id)
"/" (if item.picked { "unpick" } else { "pick" }) }
hx-target="closest tr"
hx-swap="outerHTML"
."inline-block"
."p-2"
."m-0"
."w-full"
."justify-center"
."content-center"
."flex"
."bg-green-200"[item.picked]
."hover:bg-green-100"[!item.picked]
."hover:bg-red-100"[item.picked]
{
@if item.picked {
span
."mdi"
."mdi-clipboard-text-outline"
."text-2xl"
{}
} @else {
span
."mdi"
."mdi-clipboard-text-off-outline"
."text-2xl"
{}
}
}
}
td
."border"
."p-0"
{
@if item.picked {
a
href={
"/trip/" (trip_id)
"/items/" (item.item.id)
"/" (if item.packed { "unpack" } else { "pack" }) }
hx-post={
"/trip/" (trip_id)
"/items/" (item.item.id)
"/" (if item.packed { "unpack" } else { "pack" }) }
hx-target="closest tr"
hx-swap="outerHTML"
."inline-block"
."p-2"
."m-0"
."w-full"
."justify-center"
."content-center"
."flex"
."bg-green-200"[item.packed]
."hover:bg-green-100"[!item.packed]
."hover:bg-red-100"[item.packed]
{
@if item.packed {
span
."mdi"
."mdi-bag-personal-outline"
."text-2xl"
{}
} @else {
span
."mdi"
."mdi-bag-personal-off-outline"
."text-2xl"
{}
}
}
} @else {
div
."flex"
."justify-center"
."items-center"
{
span
."mdi"
."mdi-bag-personal-outline"
."text-2xl"
."text-gray-300"
{}
}
}
}
td ."border" ."p-0" {
div
."flex"
."flex-row"
."items-center"
{
a
."p-2" ."w-full" ."inline-block"
href=(
format!("/inventory/item/{id}/", id=item.item.id)
)
{
(item.item.name.clone())
}
@if item.new {
div ."mr-2" {
span
."mdi"
."mdi-exclamation-thick"
."text-xl"
."text-yellow-400"
."grow-0"
{}
}
}
}
}
td ."border" ."p-2" style="position:relative;" {
p { (item.item.weight.to_string()) }
div ."bg-blue-600" ."h-1.5" style=(format!("
width: {width}%;
position:absolute;
left:0;
bottom:0;
right:0;", width=((item.item.weight as f64) / (biggest_item_weight as f64) * 100.0))) {}
}
}
)
}
}

View File

@@ -48,7 +48,7 @@ impl TypeList {
name="new-value"
form="edit-trip-type"
value=(trip_type.name)
;
{}
}
div
."flex"
@@ -66,7 +66,7 @@ impl TypeList {
."mdi-cancel"
."text-xl"
."m-auto"
;
{}
}
button
type="submit"
@@ -79,7 +79,7 @@ impl TypeList {
."mdi"
."mdi-content-save"
."text-xl"
;
{}
}
}
} @else {
@@ -105,7 +105,8 @@ impl TypeList {
."m-auto"
."mdi"
."mdi-pencil"
."text-xl";
."text-xl"
{}
}
}
}

View File

@@ -9,6 +9,8 @@ use axum::{
Form, Router,
};
use maud::html;
use std::str::FromStr;
use serde_variant::to_variant_name;
@@ -128,12 +130,25 @@ async fn main() -> Result<(), sqlx::Error> {
"/trip/:id/edit/:attribute/submit",
post(trip_edit_attribute),
)
.route("/trip/:id/items/:id/pick", get(trip_item_set_pick))
.route("/trip/:id/items/:id/unpick", get(trip_item_set_unpick))
.route("/trip/:id/items/:id/pack", get(trip_item_set_pack))
.route("/trip/:id/items/:id/unpack", get(trip_item_set_unpack))
.route(
"/trip/:id/items/:id/pick",
get(trip_item_set_pick).post(trip_item_set_pick_htmx),
)
.route(
"/trip/:id/items/:id/unpick",
get(trip_item_set_unpick).post(trip_item_set_unpick_htmx),
)
.route(
"/trip/:id/items/:id/pack",
get(trip_item_set_pack).post(trip_item_set_pack_htmx),
)
.route(
"/trip/:id/items/:id/unpack",
get(trip_item_set_unpack).post(trip_item_set_unpack_htmx),
)
.route("/inventory/", get(inventory_inactive))
.route("/inventory/category/", post(inventory_category_create))
.route("/inventory/item/:id/", get(inventory_item))
.route("/inventory/item/", post(inventory_item_create))
.route(
"/inventory/item/name/validate",
@@ -1007,6 +1022,14 @@ async fn trip_item_set_pick(
)?
}
async fn trip_item_set_pick_htmx(
State(state): State<AppState>,
Path((trip_id, item_id)): Path<(Uuid, Uuid)>,
) -> Result<(StatusCode, Markup), (StatusCode, Markup)> {
trip_item_set_state(&state, trip_id, item_id, TripItemStateKey::Pick, true).await?;
Ok((StatusCode::OK, trip_row(&state, trip_id, item_id).await?))
}
async fn trip_item_set_unpick(
State(state): State<AppState>,
Path((trip_id, item_id)): Path<(Uuid, Uuid)>,
@@ -1033,6 +1056,51 @@ async fn trip_item_set_unpick(
)?
}
async fn trip_row(
state: &AppState,
trip_id: Uuid,
item_id: Uuid,
) -> Result<Markup, (StatusCode, Markup)> {
let item: TripItem = TripItem::find(&state.database_pool, trip_id, item_id)
.await
.map_err(|error| {
(
StatusCode::BAD_REQUEST,
ErrorPage::build(&error.to_string()),
)
})?
.ok_or_else(|| {
(
StatusCode::NOT_FOUND,
ErrorPage::build(&format!(
"item with id {} not found for trip {}",
item_id, trip_id
)),
)
})?;
Ok(components::trip::TripItemListRow::build(
trip_id,
&item,
Item::get_category_max_weight(&state.database_pool, item.item.category_id)
.await
.map_err(|error| {
(
StatusCode::BAD_REQUEST,
ErrorPage::build(&error.to_string()),
)
})?,
))
}
async fn trip_item_set_unpick_htmx(
State(state): State<AppState>,
Path((trip_id, item_id)): Path<(Uuid, Uuid)>,
) -> Result<(StatusCode, Markup), (StatusCode, Markup)> {
trip_item_set_state(&state, trip_id, item_id, TripItemStateKey::Pick, false).await?;
Ok((StatusCode::OK, trip_row(&state, trip_id, item_id).await?))
}
async fn trip_item_set_pack(
State(state): State<AppState>,
Path((trip_id, item_id)): Path<(Uuid, Uuid)>,
@@ -1059,6 +1127,14 @@ async fn trip_item_set_pack(
)?
}
async fn trip_item_set_pack_htmx(
State(state): State<AppState>,
Path((trip_id, item_id)): Path<(Uuid, Uuid)>,
) -> Result<(StatusCode, Markup), (StatusCode, Markup)> {
trip_item_set_state(&state, trip_id, item_id, TripItemStateKey::Pack, true).await?;
Ok((StatusCode::OK, trip_row(&state, trip_id, item_id).await?))
}
async fn trip_item_set_unpack(
State(state): State<AppState>,
Path((trip_id, item_id)): Path<(Uuid, Uuid)>,
@@ -1085,6 +1161,14 @@ async fn trip_item_set_unpack(
)?
}
async fn trip_item_set_unpack_htmx(
State(state): State<AppState>,
Path((trip_id, item_id)): Path<(Uuid, Uuid)>,
) -> Result<(StatusCode, Markup), (StatusCode, Markup)> {
trip_item_set_state(&state, trip_id, item_id, TripItemStateKey::Pack, false).await?;
Ok((StatusCode::OK, trip_row(&state, trip_id, item_id).await?))
}
#[derive(Deserialize)]
struct NewCategory {
#[serde(rename = "new-category-name")]
@@ -1339,3 +1423,59 @@ async fn trips_types_edit_name(
Ok(Redirect::to("/trips/types/"))
}
}
async fn inventory_item(
State(state): State<AppState>,
Path(id): Path<Uuid>,
) -> Result<(StatusCode, Markup), (StatusCode, Markup)> {
let id_param = id.to_string();
let item: models::InventoryItem = query_as!(
DbInventoryItemRow,
"SELECT
item.id AS id,
item.name AS name,
item.description AS description,
weight,
category.id AS category_id,
category.name AS category_name,
category.description AS category_description,
product.id AS product_id,
product.name AS product_name,
product.description AS product_description,
product.comment AS product_comment
FROM inventory_items AS item
INNER JOIN inventory_items_categories as category
ON item.category_id = category.id
LEFT JOIN inventory_products AS product
ON item.product_id = product.id
WHERE item.id = ?",
id_param,
)
.fetch_one(&state.database_pool)
.map_ok(|row| row.try_into())
.await
.map_err(|e: sqlx::Error| match e {
sqlx::Error::RowNotFound => (
StatusCode::NOT_FOUND,
ErrorPage::build(&format!("item with id {} not found", id)),
),
_ => (
StatusCode::INTERNAL_SERVER_ERROR,
ErrorPage::build(&e.to_string()),
),
})?
.map_err(|e: Error| {
(
StatusCode::INTERNAL_SERVER_ERROR,
ErrorPage::build(&e.to_string()),
)
})?;
Ok((
StatusCode::OK,
Root::build(
&components::InventoryItem::build(&state.client_state, &item),
&TopLevelPage::Inventory,
),
))
}

View File

@@ -224,6 +224,79 @@ pub struct TripItem {
pub new: bool,
}
pub struct DbTripsItemsRow {
picked: bool,
packed: bool,
new: bool,
id: String,
name: String,
weight: i64,
description: Option<String>,
category_id: String,
}
impl TryFrom<DbTripsItemsRow> for TripItem {
type Error = Error;
fn try_from(row: DbTripsItemsRow) -> Result<Self, Self::Error> {
Ok(TripItem {
picked: row.picked,
packed: row.packed,
new: row.new,
item: Item {
id: Uuid::try_parse(&row.id)?,
name: row.name,
description: row.description,
weight: row.weight,
category_id: Uuid::try_parse(&row.category_id)?,
},
})
}
}
impl TripItem {
pub async fn find(
pool: &sqlx::Pool<sqlx::Sqlite>,
trip_id: Uuid,
item_id: Uuid,
) -> Result<Option<Self>, Error> {
let item_id_param = item_id.to_string();
let trip_id_param = trip_id.to_string();
let item: Result<Result<TripItem, Error>, sqlx::Error> = sqlx::query_as!(
DbTripsItemsRow,
"
SELECT
t_item.item_id AS id,
t_item.pick AS picked,
t_item.pack AS packed,
t_item.new AS new,
i_item.name AS name,
i_item.description AS description,
i_item.weight AS weight,
i_item.category_id AS category_id
FROM trips_items AS t_item
INNER JOIN inventory_items AS i_item
ON i_item.id = t_item.item_id
WHERE t_item.item_id = ?
AND t_item.trip_id = ?
",
item_id_param,
trip_id_param,
)
.fetch_one(pool)
.map_ok(|row| row.try_into())
.await;
match item {
Err(e) => match e {
sqlx::Error::RowNotFound => Ok(None),
_ => Err(e.into()),
},
Ok(v) => Ok(Some(v?)),
}
}
}
pub struct DbTripRow {
pub id: String,
pub name: String,
@@ -652,7 +725,13 @@ impl Item {
let id_param = id.to_string();
let item: Result<Result<Item, Error>, sqlx::Error> = sqlx::query_as!(
DbInventoryItemsRow,
"SELECT * FROM inventory_items AS item
"SELECT
id,
name,
weight,
description,
category_id
FROM inventory_items AS item
WHERE item.id = ?",
id_param,
)
@@ -705,6 +784,69 @@ impl Item {
Ok(v) => Ok(Some(v?)),
}
}
pub async fn get_category_max_weight(
pool: &sqlx::Pool<sqlx::Sqlite>,
id: Uuid,
) -> Result<i64, Error> {
let id_param = id.to_string();
let weight: Result<i64, sqlx::Error> = sqlx::query!(
"
SELECT COALESCE(MAX(i_item.weight), 0) as weight
FROM inventory_items_categories as category
INNER JOIN inventory_items as i_item
ON i_item.category_id = category.id
WHERE category_id = (
SELECT category_id
FROM inventory_items
WHERE inventory_items.id = ?
)
",
id_param
)
.fetch_one(pool)
.map_ok(|row| {
// convert to i64 because that the default integer type, but looks
// like COALESCE return i32?
//
// We can be certain that the row exists, as we COALESCE it
row.weight.unwrap() as i64
})
.await;
Ok(weight?)
}
pub async fn _get_category_total_picked_weight(
pool: &sqlx::Pool<sqlx::Sqlite>,
category_id: Uuid,
) -> Result<i64, Error> {
let category_id_param = category_id.to_string();
let weight: Result<i64, sqlx::Error> = sqlx::query!(
"
SELECT COALESCE(SUM(i_item.weight), 0) as weight
FROM inventory_items_categories as category
INNER JOIN inventory_items as i_item
ON i_item.category_id = category.id
INNER JOIN trips_items as t_item
ON i_item.id = t_item.item_id
WHERE category_id = ?
AND t_item.pick = 1
",
category_id_param
)
.fetch_one(pool)
.map_ok(|row| {
// convert to i64 because that the default integer type, but looks
// like COALESCE return i32?
//
// We can be certain that the row exists, as we COALESCE it
row.weight.unwrap() as i64
})
.await;
Ok(weight?)
}
}
pub struct DbTripsTypesRow {
@@ -733,3 +875,63 @@ impl TryFrom<DbTripsTypesRow> for TripsType {
})
}
}
pub struct Product {
pub id: Uuid,
pub name: String,
pub description: Option<String>,
pub comment: Option<String>,
}
pub struct InventoryItem {
pub id: Uuid,
pub name: String,
pub description: Option<String>,
pub weight: i64,
pub category: Category,
pub product: Option<Product>,
}
pub struct DbInventoryItemRow {
pub id: String,
pub name: String,
pub description: Option<String>,
pub weight: i64,
pub category_id: String,
pub category_name: String,
pub category_description: Option<String>,
pub product_id: Option<String>,
pub product_name: Option<String>,
pub product_description: Option<String>,
pub product_comment: Option<String>,
}
impl TryFrom<DbInventoryItemRow> for InventoryItem {
type Error = Error;
fn try_from(row: DbInventoryItemRow) -> Result<Self, Self::Error> {
Ok(InventoryItem {
id: Uuid::try_parse(&row.id)?,
name: row.name,
description: row.description,
weight: row.weight,
category: Category {
id: Uuid::try_parse(&row.category_id)?,
name: row.category_name,
description: row.category_description,
items: None,
},
product: row
.product_id
.map(|id| -> Result<Product, Error> {
Ok(Product {
id: Uuid::try_parse(&id)?,
name: row.product_name.unwrap(),
description: row.product_description,
comment: row.product_comment,
})
})
.transpose()?,
})
}
}