update
This commit is contained in:
@@ -23,13 +23,13 @@ impl Root {
|
|||||||
html {
|
html {
|
||||||
head {
|
head {
|
||||||
title { "Packager" }
|
title { "Packager" }
|
||||||
script src="https://unpkg.com/htmx.org@1.7.0" {}
|
script src="https://unpkg.com/htmx.org@1.9.2" {}
|
||||||
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";
|
||||||
script { (include_str!(concat!(env!("CARGO_MANIFEST_DIR"),"/js/app.js"))) }
|
script { (include_str!(concat!(env!("CARGO_MANIFEST_DIR"),"/js/app.js"))) }
|
||||||
}
|
}
|
||||||
body {
|
body hx-boost="true" {
|
||||||
header
|
header
|
||||||
."bg-gray-200"
|
."bg-gray-200"
|
||||||
."p-5"
|
."p-5"
|
||||||
@@ -53,13 +53,9 @@ impl Root {
|
|||||||
}} { "Trips" }
|
}} { "Trips" }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
div
|
|
||||||
hx-boost="true"
|
|
||||||
{
|
|
||||||
(body)
|
(body)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -191,20 +191,83 @@ impl NewTrip {
|
|||||||
pub struct Trip;
|
pub struct Trip;
|
||||||
|
|
||||||
impl Trip {
|
impl Trip {
|
||||||
pub fn build(state: &ClientState, trip: &models::Trip) -> Markup {
|
pub fn build(state: &ClientState, trip: &models::Trip) -> Result<Markup, Error> {
|
||||||
html!(
|
Ok(html!(
|
||||||
div ."p-8" {
|
div ."p-8" ."flex" ."flex-col" ."gap-8" {
|
||||||
div ."flex" ."flex-row" ."items-center" ."gap-x-3" {
|
div ."flex" ."flex-row" ."items-center" ."gap-x-3" {
|
||||||
|
@if state.trip_edit_attribute.as_ref().map_or(false, |a| *a == TripAttribute::Name) {
|
||||||
|
form
|
||||||
|
id="edit-trip"
|
||||||
|
action=(format!("edit/{}/submit", to_variant_name(&TripAttribute::Name).unwrap()))
|
||||||
|
target="_self"
|
||||||
|
method="post"
|
||||||
|
{
|
||||||
|
div
|
||||||
|
."flex"
|
||||||
|
."flex-row"
|
||||||
|
."items-center"
|
||||||
|
."gap-x-3"
|
||||||
|
."items-stretch"
|
||||||
|
{
|
||||||
|
input
|
||||||
|
."bg-blue-200"
|
||||||
|
."w-full"
|
||||||
|
."text-2xl"
|
||||||
|
."font-semibold"
|
||||||
|
type=(<InputType as Into<&'static str>>::into(InputType::Text))
|
||||||
|
name="new-value"
|
||||||
|
form="edit-trip"
|
||||||
|
value=(trip.name)
|
||||||
|
;
|
||||||
|
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"
|
||||||
|
."bg-green-200"
|
||||||
|
."hover:bg-green-300"
|
||||||
|
."w-8"
|
||||||
|
{
|
||||||
|
span
|
||||||
|
."mdi"
|
||||||
|
."mdi-content-save"
|
||||||
|
."text-xl"
|
||||||
|
;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} @else {
|
||||||
h1 ."text-2xl" ."font-semibold"{ (trip.name) }
|
h1 ."text-2xl" ."font-semibold"{ (trip.name) }
|
||||||
|
span {
|
||||||
|
a href=(format!("?edit={}", to_variant_name(&TripAttribute::Name).unwrap()))
|
||||||
|
{
|
||||||
|
span
|
||||||
|
."mdi"
|
||||||
|
."mdi-pencil"
|
||||||
|
."text-xl"
|
||||||
|
."opacity-50"
|
||||||
|
;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
div ."my-6" {
|
|
||||||
(TripInfo::build(state, &trip))
|
(TripInfo::build(state, &trip))
|
||||||
}
|
|
||||||
div ."my-6" {
|
|
||||||
(TripComment::build(&trip))
|
(TripComment::build(&trip))
|
||||||
|
(TripItems::build(state, &trip)?)
|
||||||
}
|
}
|
||||||
}
|
))
|
||||||
)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -225,8 +288,8 @@ impl TripInfoRow {
|
|||||||
name="edit-trip"
|
name="edit-trip"
|
||||||
id="edit-trip"
|
id="edit-trip"
|
||||||
action=(format!("edit/{key}/submit", key=(to_variant_name(&attribute_key).unwrap()) ))
|
action=(format!("edit/{key}/submit", key=(to_variant_name(&attribute_key).unwrap()) ))
|
||||||
// hx-post=(format!("edit/{name}/submit"))
|
htmx-push-url="true"
|
||||||
target="."
|
target="_self"
|
||||||
method="post"
|
method="post"
|
||||||
;
|
;
|
||||||
}
|
}
|
||||||
@@ -410,6 +473,10 @@ impl TripInfo {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
tr .h-full {
|
||||||
|
td ."border" ."p-2" { "Carried weight" }
|
||||||
|
td ."border" ."p-2" { "TODO" }
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
@@ -421,6 +488,7 @@ pub struct TripComment;
|
|||||||
impl TripComment {
|
impl TripComment {
|
||||||
pub fn build(trip: &models::Trip) -> Markup {
|
pub fn build(trip: &models::Trip) -> Markup {
|
||||||
html!(
|
html!(
|
||||||
|
div {
|
||||||
h1 ."text-xl" ."mb-5" { "Comments" }
|
h1 ."text-xl" ."mb-5" { "Comments" }
|
||||||
|
|
||||||
form
|
form
|
||||||
@@ -458,6 +526,253 @@ impl TripComment {
|
|||||||
span ."mdi" ."mdi-content-save" ."text-xl";
|
span ."mdi" ."mdi-content-save" ."text-xl";
|
||||||
span { "Save" }
|
span { "Save" }
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub struct TripItems;
|
||||||
|
|
||||||
|
impl TripItems {
|
||||||
|
pub fn build(state: &ClientState, trip: &models::Trip) -> Result<Markup, Error> {
|
||||||
|
Ok(html!(
|
||||||
|
div ."grid" ."grid-cols-4" ."gap-3" {
|
||||||
|
div ."col-span-2" {
|
||||||
|
(TripCategoryList::build(state, &trip))
|
||||||
|
}
|
||||||
|
div ."col-span-2" {
|
||||||
|
h1 ."text-2xl" ."mb-5" ."text-center" { "Items" }
|
||||||
|
@if let Some(active_category_id) = state.active_category_id {
|
||||||
|
(TripItemList::build(
|
||||||
|
&state,
|
||||||
|
&trip,
|
||||||
|
&trip
|
||||||
|
.categories()
|
||||||
|
.iter()
|
||||||
|
.find(|category|
|
||||||
|
category.category.id == active_category_id
|
||||||
|
)
|
||||||
|
.ok_or(
|
||||||
|
Error::NotFoundError {
|
||||||
|
description: format!("no category with id {}", active_category_id)
|
||||||
|
}
|
||||||
|
)?
|
||||||
|
.items
|
||||||
|
.as_ref()
|
||||||
|
.unwrap()
|
||||||
|
)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub struct TripCategoryList;
|
||||||
|
|
||||||
|
impl TripCategoryList {
|
||||||
|
pub fn build(state: &ClientState, trip: &models::Trip) -> Markup {
|
||||||
|
let categories = trip.categories();
|
||||||
|
|
||||||
|
let biggest_category_weight: u32 = categories
|
||||||
|
.iter()
|
||||||
|
.map(TripCategory::total_picked_weight)
|
||||||
|
.max()
|
||||||
|
.unwrap_or(1);
|
||||||
|
|
||||||
|
html!(
|
||||||
|
h1 ."text-2xl" ."mb-5" ."text-center" { "Categories" }
|
||||||
|
table
|
||||||
|
."table"
|
||||||
|
."table-auto"
|
||||||
|
."border-collapse"
|
||||||
|
."border-spacing-0"
|
||||||
|
."border"
|
||||||
|
."w-full"
|
||||||
|
{
|
||||||
|
colgroup {
|
||||||
|
col style="width:50%" {}
|
||||||
|
col style="width:50%" {}
|
||||||
|
}
|
||||||
|
thead ."bg-gray-200" {
|
||||||
|
tr ."h-10" {
|
||||||
|
th ."border" ."p-2" ."w-2/5" { "Name" }
|
||||||
|
th ."border" ."p-2" { "Weight" }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
tbody {
|
||||||
|
@for category in trip.categories() {
|
||||||
|
@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]
|
||||||
|
."pointer-events-none"[active]
|
||||||
|
{
|
||||||
|
|
||||||
|
td
|
||||||
|
class=@if state.active_category_id.map_or(false, |id| category.category.id == id) {
|
||||||
|
"border p-0 m-0 font-bold"
|
||||||
|
} @else {
|
||||||
|
"border p-0 m-0"
|
||||||
|
} {
|
||||||
|
a
|
||||||
|
id="select-category"
|
||||||
|
href=(
|
||||||
|
format!(
|
||||||
|
"?category={id}",
|
||||||
|
id=category.category.id
|
||||||
|
)
|
||||||
|
)
|
||||||
|
."inline-block" ."p-2" ."m-0" ."w-full"
|
||||||
|
{
|
||||||
|
(category.category.name.clone())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
td ."border" ."p-2" ."m-0" 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=(
|
||||||
|
f64::from(category.total_picked_weight())
|
||||||
|
/ f64::from(biggest_category_weight)
|
||||||
|
* 100.0
|
||||||
|
)
|
||||||
|
)
|
||||||
|
) {}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
tr ."h-10" ."hover:bg-purple-200" ."bg-gray-300" ."font-bold" {
|
||||||
|
td ."border" ."p-0" ."m-0" {
|
||||||
|
p ."p-2" ."m-2" { "Sum" }
|
||||||
|
}
|
||||||
|
td ."border" ."p-0" ."m-0" {
|
||||||
|
p ."p-2" ."m-2" {
|
||||||
|
(categories.iter().map(TripCategory::total_picked_weight).sum::<u32>().to_string())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub struct TripItemList;
|
||||||
|
|
||||||
|
impl TripItemList {
|
||||||
|
pub fn build(state: &ClientState, trip: &models::Trip, items: &Vec<TripItem>) -> Markup {
|
||||||
|
let biggest_item_weight: u32 = items.iter().map(|item| item.item.weight).max().unwrap_or(1);
|
||||||
|
|
||||||
|
html!(
|
||||||
|
@if items.is_empty() {
|
||||||
|
p ."text-lg" ."text-center" ."py-5" ."text-gray-400" { "[Empty]" }
|
||||||
|
} @else {
|
||||||
|
@if let Some(edit_item) = state.edit_item {
|
||||||
|
form
|
||||||
|
name="edit-item"
|
||||||
|
id="edit-item"
|
||||||
|
action=(format!("/inventory/item/{edit_item}/edit"))
|
||||||
|
target="_self"
|
||||||
|
method="post"
|
||||||
|
{}
|
||||||
|
}
|
||||||
|
table
|
||||||
|
."table"
|
||||||
|
."table-auto"
|
||||||
|
.table-fixed
|
||||||
|
."border-collapse"
|
||||||
|
."border-spacing-0"
|
||||||
|
."border"
|
||||||
|
."w-full"
|
||||||
|
{
|
||||||
|
thead ."bg-gray-200" {
|
||||||
|
tr ."h-10" {
|
||||||
|
th ."border" ."p-2" { "Take?" }
|
||||||
|
th ."border" ."p-2" { "Packed?" }
|
||||||
|
th ."border" ."p-2" ."w-1/2" { "Name" }
|
||||||
|
th ."border" ."p-2" ."w-1/4" { "Weight" }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
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" {
|
||||||
|
a
|
||||||
|
."p-2" ."w-full" ."inline-block"
|
||||||
|
href=(
|
||||||
|
format!("/inventory/item/{id}/", id=item.item.id)
|
||||||
|
) {
|
||||||
|
|
||||||
|
(item.item.name.clone())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
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=(f64::from(item.item.weight) / f64::from(biggest_item_weight) * 100.0))) {}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
164
rust/src/main.rs
164
rust/src/main.rs
@@ -97,6 +97,10 @@ async fn main() -> Result<(), sqlx::Error> {
|
|||||||
"/trip/:id/edit/:attribute/submit",
|
"/trip/:id/edit/:attribute/submit",
|
||||||
post(trip_edit_attribute),
|
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("/inventory/", get(inventory_inactive))
|
.route("/inventory/", get(inventory_inactive))
|
||||||
.route("/inventory/item/", post(inventory_item_create))
|
.route("/inventory/item/", post(inventory_item_create))
|
||||||
.route("/inventory/category/:id/", get(inventory_active))
|
.route("/inventory/category/:id/", get(inventory_active))
|
||||||
@@ -511,6 +515,7 @@ async fn trips(
|
|||||||
#[derive(Debug, Deserialize)]
|
#[derive(Debug, Deserialize)]
|
||||||
struct TripQuery {
|
struct TripQuery {
|
||||||
edit: Option<TripAttribute>,
|
edit: Option<TripAttribute>,
|
||||||
|
category: Option<Uuid>,
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn trip(
|
async fn trip(
|
||||||
@@ -519,6 +524,7 @@ async fn trip(
|
|||||||
Query(trip_query): Query<TripQuery>,
|
Query(trip_query): Query<TripQuery>,
|
||||||
) -> Result<(StatusCode, Markup), (StatusCode, Markup)> {
|
) -> Result<(StatusCode, Markup), (StatusCode, Markup)> {
|
||||||
state.client_state.trip_edit_attribute = trip_query.edit;
|
state.client_state.trip_edit_attribute = trip_query.edit;
|
||||||
|
state.client_state.active_category_id = trip_query.category;
|
||||||
|
|
||||||
let mut trip: models::Trip =
|
let mut trip: models::Trip =
|
||||||
query("SELECT id,name,date_start,date_end,state,location,temp_min,temp_max,comment FROM trips WHERE id = ?")
|
query("SELECT id,name,date_start,date_end,state,location,temp_min,temp_max,comment FROM trips WHERE id = ?")
|
||||||
@@ -544,10 +550,27 @@ async fn trip(
|
|||||||
)
|
)
|
||||||
})?;
|
})?;
|
||||||
|
|
||||||
|
trip.load_categories(&state.database_pool)
|
||||||
|
.await
|
||||||
|
.map_err(|e| {
|
||||||
|
(
|
||||||
|
StatusCode::INTERNAL_SERVER_ERROR,
|
||||||
|
ErrorPage::build(&e.to_string()),
|
||||||
|
)
|
||||||
|
})?;
|
||||||
|
|
||||||
Ok((
|
Ok((
|
||||||
StatusCode::OK,
|
StatusCode::OK,
|
||||||
Root::build(
|
Root::build(
|
||||||
components::Trip::build(&state.client_state, &trip),
|
components::Trip::build(&state.client_state, &trip).map_err(|e| match e {
|
||||||
|
Error::NotFoundError { description } => {
|
||||||
|
(StatusCode::NOT_FOUND, ErrorPage::build(&description))
|
||||||
|
}
|
||||||
|
_ => (
|
||||||
|
StatusCode::INTERNAL_SERVER_ERROR,
|
||||||
|
ErrorPage::build(&e.to_string()),
|
||||||
|
),
|
||||||
|
})?,
|
||||||
&TopLevelPage::Trips,
|
&TopLevelPage::Trips,
|
||||||
),
|
),
|
||||||
))
|
))
|
||||||
@@ -703,6 +726,143 @@ async fn trip_edit_attribute(
|
|||||||
ErrorPage::build(&format!("trip with id {id} not found", id = trip_id)),
|
ErrorPage::build(&format!("trip with id {id} not found", id = trip_id)),
|
||||||
))
|
))
|
||||||
} else {
|
} else {
|
||||||
Ok(Redirect::to(&format!("/trips/")))
|
Ok(Redirect::to(&format!("/trip/{trip_id}/")))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async fn trip_item_set_state(
|
||||||
|
state: &AppState,
|
||||||
|
trip_id: Uuid,
|
||||||
|
item_id: Uuid,
|
||||||
|
key: TripItemStateKey,
|
||||||
|
value: bool,
|
||||||
|
) -> Result<(), (StatusCode, Markup)> {
|
||||||
|
let result = query(&format!(
|
||||||
|
"UPDATE tripitems
|
||||||
|
SET {key} = ?
|
||||||
|
WHERE trip_id = ?
|
||||||
|
AND item_id = ?",
|
||||||
|
key = to_variant_name(&key).unwrap()
|
||||||
|
))
|
||||||
|
.bind(value)
|
||||||
|
.bind(trip_id.to_string())
|
||||||
|
.bind(item_id.to_string())
|
||||||
|
.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!(
|
||||||
|
"trip with id {trip_id} or item with id {item_id} not found"
|
||||||
|
)),
|
||||||
|
))
|
||||||
|
} else {
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn trip_item_set_pick(
|
||||||
|
Path((trip_id, item_id)): Path<(Uuid, Uuid)>,
|
||||||
|
headers: HeaderMap,
|
||||||
|
State(state): State<AppState>,
|
||||||
|
) -> Result<Redirect, (StatusCode, Markup)> {
|
||||||
|
Ok(trip_item_set_state(&state, trip_id, item_id, TripItemStateKey::Pick, true).await?).map(
|
||||||
|
|_| -> Result<Redirect, (StatusCode, Markup)> {
|
||||||
|
Ok(Redirect::to(
|
||||||
|
headers
|
||||||
|
.get("referer")
|
||||||
|
.ok_or((
|
||||||
|
StatusCode::BAD_REQUEST,
|
||||||
|
ErrorPage::build("no referer header found"),
|
||||||
|
))?
|
||||||
|
.to_str()
|
||||||
|
.map_err(|e| {
|
||||||
|
(
|
||||||
|
StatusCode::BAD_REQUEST,
|
||||||
|
ErrorPage::build(&format!("referer could not be converted: {}", e)),
|
||||||
|
)
|
||||||
|
})?,
|
||||||
|
))
|
||||||
|
},
|
||||||
|
)?
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn trip_item_set_unpick(
|
||||||
|
Path((trip_id, item_id)): Path<(Uuid, Uuid)>,
|
||||||
|
headers: HeaderMap,
|
||||||
|
State(state): State<AppState>,
|
||||||
|
) -> Result<Redirect, (StatusCode, Markup)> {
|
||||||
|
Ok(trip_item_set_state(&state, trip_id, item_id, TripItemStateKey::Pick, false).await?).map(
|
||||||
|
|_| -> Result<Redirect, (StatusCode, Markup)> {
|
||||||
|
Ok(Redirect::to(
|
||||||
|
headers
|
||||||
|
.get("referer")
|
||||||
|
.ok_or((
|
||||||
|
StatusCode::BAD_REQUEST,
|
||||||
|
ErrorPage::build("no referer header found"),
|
||||||
|
))?
|
||||||
|
.to_str()
|
||||||
|
.map_err(|e| {
|
||||||
|
(
|
||||||
|
StatusCode::BAD_REQUEST,
|
||||||
|
ErrorPage::build(&format!("referer could not be converted: {}", e)),
|
||||||
|
)
|
||||||
|
})?,
|
||||||
|
))
|
||||||
|
},
|
||||||
|
)?
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn trip_item_set_pack(
|
||||||
|
Path((trip_id, item_id)): Path<(Uuid, Uuid)>,
|
||||||
|
headers: HeaderMap,
|
||||||
|
State(state): State<AppState>,
|
||||||
|
) -> Result<Redirect, (StatusCode, Markup)> {
|
||||||
|
Ok(trip_item_set_state(&state, trip_id, item_id, TripItemStateKey::Pack, true).await?).map(
|
||||||
|
|_| -> Result<Redirect, (StatusCode, Markup)> {
|
||||||
|
Ok(Redirect::to(
|
||||||
|
headers
|
||||||
|
.get("referer")
|
||||||
|
.ok_or((
|
||||||
|
StatusCode::BAD_REQUEST,
|
||||||
|
ErrorPage::build("no referer header found"),
|
||||||
|
))?
|
||||||
|
.to_str()
|
||||||
|
.map_err(|e| {
|
||||||
|
(
|
||||||
|
StatusCode::BAD_REQUEST,
|
||||||
|
ErrorPage::build(&format!("referer could not be converted: {}", e)),
|
||||||
|
)
|
||||||
|
})?,
|
||||||
|
))
|
||||||
|
},
|
||||||
|
)?
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn trip_item_set_unpack(
|
||||||
|
Path((trip_id, item_id)): Path<(Uuid, Uuid)>,
|
||||||
|
headers: HeaderMap,
|
||||||
|
State(state): State<AppState>,
|
||||||
|
) -> Result<Redirect, (StatusCode, Markup)> {
|
||||||
|
Ok(trip_item_set_state(&state, trip_id, item_id, TripItemStateKey::Pack, false).await?).map(
|
||||||
|
|_| -> Result<Redirect, (StatusCode, Markup)> {
|
||||||
|
Ok(Redirect::to(
|
||||||
|
headers
|
||||||
|
.get("referer")
|
||||||
|
.ok_or((
|
||||||
|
StatusCode::BAD_REQUEST,
|
||||||
|
ErrorPage::build("no referer header found"),
|
||||||
|
))?
|
||||||
|
.to_str()
|
||||||
|
.map_err(|e| {
|
||||||
|
(
|
||||||
|
StatusCode::BAD_REQUEST,
|
||||||
|
ErrorPage::build(&format!("referer could not be converted: {}", e)),
|
||||||
|
)
|
||||||
|
})?,
|
||||||
|
))
|
||||||
|
},
|
||||||
|
)?
|
||||||
|
}
|
||||||
|
|||||||
@@ -88,6 +88,50 @@ impl fmt::Display for TripState {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[derive(Serialize, Debug)]
|
||||||
|
pub enum TripItemStateKey {
|
||||||
|
Pick,
|
||||||
|
Pack,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl fmt::Display for TripItemStateKey {
|
||||||
|
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
|
||||||
|
write!(
|
||||||
|
f,
|
||||||
|
"{}",
|
||||||
|
match self {
|
||||||
|
Self::Pick => "pick",
|
||||||
|
Self::Pack => "pack",
|
||||||
|
},
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug)]
|
||||||
|
pub struct TripCategory {
|
||||||
|
pub category: Category,
|
||||||
|
pub items: Option<Vec<TripItem>>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl TripCategory {
|
||||||
|
pub fn total_picked_weight(&self) -> u32 {
|
||||||
|
self.items
|
||||||
|
.as_ref()
|
||||||
|
.unwrap()
|
||||||
|
.iter()
|
||||||
|
.filter(|item| item.picked)
|
||||||
|
.map(|item| item.item.weight)
|
||||||
|
.sum()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug)]
|
||||||
|
pub struct TripItem {
|
||||||
|
pub item: Item,
|
||||||
|
pub picked: bool,
|
||||||
|
pub packed: bool,
|
||||||
|
}
|
||||||
|
|
||||||
pub struct Trip {
|
pub struct Trip {
|
||||||
pub id: Uuid,
|
pub id: Uuid,
|
||||||
pub name: String,
|
pub name: String,
|
||||||
@@ -99,10 +143,13 @@ pub struct Trip {
|
|||||||
pub temp_max: i32,
|
pub temp_max: i32,
|
||||||
pub comment: Option<String>,
|
pub comment: Option<String>,
|
||||||
types: Option<Vec<TripType>>,
|
types: Option<Vec<TripType>>,
|
||||||
|
categories: Option<Vec<TripCategory>>,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq)]
|
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq)]
|
||||||
pub enum TripAttribute {
|
pub enum TripAttribute {
|
||||||
|
#[serde(rename = "name")]
|
||||||
|
Name,
|
||||||
#[serde(rename = "date_start")]
|
#[serde(rename = "date_start")]
|
||||||
DateStart,
|
DateStart,
|
||||||
#[serde(rename = "date_end")]
|
#[serde(rename = "date_end")]
|
||||||
@@ -173,6 +220,7 @@ impl TryFrom<SqliteRow> for Trip {
|
|||||||
temp_max,
|
temp_max,
|
||||||
comment,
|
comment,
|
||||||
types: None,
|
types: None,
|
||||||
|
categories: None,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -183,6 +231,12 @@ impl<'a> Trip {
|
|||||||
.as_ref()
|
.as_ref()
|
||||||
.expect("you need to call load_triptypes()")
|
.expect("you need to call load_triptypes()")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn categories(&'a self) -> &Vec<TripCategory> {
|
||||||
|
self.categories
|
||||||
|
.as_ref()
|
||||||
|
.expect("you need to call load_triptypes()")
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl<'a> Trip {
|
impl<'a> Trip {
|
||||||
@@ -220,6 +274,82 @@ impl<'a> Trip {
|
|||||||
self.types = Some(types);
|
self.types = Some(types);
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub async fn load_categories(
|
||||||
|
&'a mut self,
|
||||||
|
pool: &sqlx::Pool<sqlx::Sqlite>,
|
||||||
|
) -> Result<(), Error> {
|
||||||
|
let mut categories: Vec<TripCategory> = vec![];
|
||||||
|
// we can ignore the return type as we collect into `categories`
|
||||||
|
// in the `map_ok()` closure
|
||||||
|
sqlx::query(
|
||||||
|
"
|
||||||
|
SELECT
|
||||||
|
category.id as category_id,
|
||||||
|
category.name as category_name,
|
||||||
|
category.description as category_description,
|
||||||
|
item.id as item_id,
|
||||||
|
item.name as item_name,
|
||||||
|
item.description as item_description,
|
||||||
|
item.weight as item_weight,
|
||||||
|
trip.pick as item_is_picked,
|
||||||
|
trip.pack as item_is_packed
|
||||||
|
FROM tripitems as trip
|
||||||
|
INNER JOIN inventoryitems as item
|
||||||
|
ON item.id = trip.item_id
|
||||||
|
INNER JOIN inventoryitemcategories as category
|
||||||
|
ON category.id = item.category_id
|
||||||
|
WHERE trip.trip_id = ?;
|
||||||
|
",
|
||||||
|
)
|
||||||
|
.bind(self.id.to_string())
|
||||||
|
.fetch(pool)
|
||||||
|
.map_ok(|row| -> Result<(), Error> {
|
||||||
|
let mut category = TripCategory {
|
||||||
|
category: Category {
|
||||||
|
id: Uuid::try_parse(row.try_get("category_id")?)?,
|
||||||
|
name: row.try_get("category_name")?,
|
||||||
|
description: row.try_get("category_description")?,
|
||||||
|
items: None,
|
||||||
|
},
|
||||||
|
items: None,
|
||||||
|
};
|
||||||
|
|
||||||
|
let item = TripItem {
|
||||||
|
item: Item {
|
||||||
|
id: Uuid::try_parse(row.try_get("item_id")?)?,
|
||||||
|
name: row.try_get("item_name")?,
|
||||||
|
description: row.try_get("item_description")?,
|
||||||
|
weight: row.try_get("item_weight")?,
|
||||||
|
category_id: category.category.id,
|
||||||
|
},
|
||||||
|
picked: row.try_get("item_is_picked")?,
|
||||||
|
packed: row.try_get("item_is_packed")?,
|
||||||
|
};
|
||||||
|
|
||||||
|
if let Some(&mut ref mut c) = categories
|
||||||
|
.iter_mut()
|
||||||
|
.find(|c| c.category.id == category.category.id)
|
||||||
|
{
|
||||||
|
// we always populate c.items when we add a new category, so
|
||||||
|
// it's safe to unwrap here
|
||||||
|
c.items.as_mut().unwrap().push(item);
|
||||||
|
} else {
|
||||||
|
category.items = Some(vec![item]);
|
||||||
|
categories.push(category);
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
})
|
||||||
|
.try_collect::<Vec<Result<(), Error>>>()
|
||||||
|
.await?
|
||||||
|
.into_iter()
|
||||||
|
.collect::<Result<(), Error>>()?;
|
||||||
|
|
||||||
|
self.categories = Some(categories);
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub struct TripType {
|
pub struct TripType {
|
||||||
|
|||||||
Reference in New Issue
Block a user