more
This commit is contained in:
@@ -10,13 +10,14 @@ impl Inventory {
|
||||
pub async fn build(state: ClientState, categories: Vec<Category>) -> Result<Markup, Error> {
|
||||
let doc = html!(
|
||||
div id="pkglist-item-manager" {
|
||||
div ."p-8" ."grid" ."grid-cols-4" ."gap-3" {
|
||||
div ."col-span-2" {
|
||||
div ."p-8" ."grid" ."grid-cols-4" ."gap-5" {
|
||||
div ."col-span-2" ."flex" ."flex-col" ."gap-8" {
|
||||
h1 ."text-2xl" ."text-center" { "Categories" }
|
||||
(InventoryCategoryList::build(&state, &categories))
|
||||
(InventoryNewCategoryForm::build())
|
||||
}
|
||||
div ."col-span-2" {
|
||||
h1 ."text-2xl" ."mb-5" ."text-center" { "Items" }
|
||||
div ."col-span-2" ."flex" ."flex-col" ."gap-8" {
|
||||
h1 ."text-2xl" ."text-center" { "Items" }
|
||||
@if let Some(active_category_id) = state.active_category_id {
|
||||
(InventoryItemList::build(&state, categories.iter().find(|category| category.id == active_category_id)
|
||||
.ok_or(Error::NotFoundError { description: format!("no category with id {}", active_category_id) })?
|
||||
@@ -44,94 +45,91 @@ impl InventoryCategoryList {
|
||||
.unwrap_or(1);
|
||||
|
||||
html!(
|
||||
div {
|
||||
h1 ."text-2xl" ."mb-5" ."text-center" { "Categories" }
|
||||
table
|
||||
."table"
|
||||
."table-auto"
|
||||
."border-collapse"
|
||||
."border-spacing-0"
|
||||
."border"
|
||||
."w-full"
|
||||
{
|
||||
table
|
||||
."table"
|
||||
."table-auto"
|
||||
."border-collapse"
|
||||
."border-spacing-0"
|
||||
."border"
|
||||
."w-full"
|
||||
{
|
||||
|
||||
colgroup {
|
||||
col style="width:50%" {}
|
||||
col style="width:50%" {}
|
||||
colgroup {
|
||||
col style="width:50%" {}
|
||||
col style="width:50%" {}
|
||||
}
|
||||
thead ."bg-gray-200" {
|
||||
tr ."h-10" {
|
||||
th ."border" ."p-2" ."w-3/5" { "Name" }
|
||||
th ."border" ."p-2" { "Weight" }
|
||||
}
|
||||
thead ."bg-gray-200" {
|
||||
tr ."h-10" {
|
||||
th ."border" ."p-2" ."w-3/5" { "Name" }
|
||||
th ."border" ."p-2" { "Weight" }
|
||||
}
|
||||
}
|
||||
tbody {
|
||||
@for category in categories {
|
||||
@let active = state.active_category_id.map_or(false, |id| 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]
|
||||
{
|
||||
}
|
||||
tbody {
|
||||
@for category in categories {
|
||||
@let active = state.active_category_id.map_or(false, |id| 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.id == id) {
|
||||
"border p-0 m-0 font-bold"
|
||||
} @else {
|
||||
"border p-0 m-0"
|
||||
} {
|
||||
a
|
||||
id="select-category"
|
||||
href=(
|
||||
format!(
|
||||
"/inventory/category/{id}/",
|
||||
id=category.id
|
||||
td
|
||||
class=@if state.active_category_id.map_or(false, |id| category.id == id) {
|
||||
"border p-0 m-0 font-bold"
|
||||
} @else {
|
||||
"border p-0 m-0"
|
||||
} {
|
||||
a
|
||||
id="select-category"
|
||||
href=(
|
||||
format!(
|
||||
"/inventory/category/{id}/",
|
||||
id=category.id
|
||||
)
|
||||
)
|
||||
// hx-post=(
|
||||
// format!(
|
||||
// "/inventory/category/{id}/items",
|
||||
// id=category.id
|
||||
// )
|
||||
// )
|
||||
// hx-swap="outerHTML"
|
||||
// hx-target="#items"
|
||||
."inline-block" ."p-2" ."m-0" ."w-full"
|
||||
{
|
||||
(category.name.clone())
|
||||
}
|
||||
}
|
||||
td ."border" ."p-2" ."m-0" style="position:relative;" {
|
||||
p {
|
||||
(category.total_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_weight() as f64)
|
||||
/ (biggest_category_weight as f64)
|
||||
* 100.0
|
||||
)
|
||||
)
|
||||
// hx-post=(
|
||||
// format!(
|
||||
// "/inventory/category/{id}/items",
|
||||
// id=category.id
|
||||
// )
|
||||
// )
|
||||
// hx-swap="outerHTML"
|
||||
// hx-target="#items"
|
||||
."inline-block" ."p-2" ."m-0" ."w-full"
|
||||
{
|
||||
(category.name.clone())
|
||||
}
|
||||
}
|
||||
td ."border" ."p-2" ."m-0" style="position:relative;" {
|
||||
p {
|
||||
(category.total_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_weight() as f64)
|
||||
/ (biggest_category_weight as f64)
|
||||
* 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(Category::total_weight).sum::<i64>().to_string())
|
||||
}
|
||||
}
|
||||
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(Category::total_weight).sum::<i64>().to_string())
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -325,7 +323,7 @@ impl InventoryNewItemForm {
|
||||
action="/inventory/item/"
|
||||
target="_self"
|
||||
method="post"
|
||||
."mt-8" ."p-5" ."border-2" ."border-gray-200" {
|
||||
."p-5" ."border-2" ."border-gray-200" {
|
||||
div ."mb-5" ."flex" ."flex-row" ."items-center" {
|
||||
span ."mdi" ."mdi-playlist-plus" ."text-2xl" ."mr-4" {}
|
||||
p ."inline" ."text-xl" { "Add new item" }
|
||||
@@ -422,7 +420,7 @@ impl InventoryNewCategoryForm {
|
||||
action="/inventory/category/"
|
||||
target="_self"
|
||||
method="post"
|
||||
."mt-8" ."p-5" ."border-2" ."border-gray-200" {
|
||||
."p-5" ."border-2" ."border-gray-200" {
|
||||
div ."mb-5" ."flex" ."flex-row" ."items-center" {
|
||||
span ."mdi" ."mdi-playlist-plus" ."text-2xl" ."mr-4" {}
|
||||
p ."inline" ."text-xl" { "Add new category" }
|
||||
|
||||
@@ -27,12 +27,14 @@ impl Root {
|
||||
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";
|
||||
script { (include_str!(concat!(env!("CARGO_MANIFEST_DIR"),"/js/app.js"))) }
|
||||
}
|
||||
body
|
||||
hx-boost="true"
|
||||
{
|
||||
header
|
||||
."h-full"
|
||||
."bg-gray-200"
|
||||
."p-5"
|
||||
."flex"
|
||||
@@ -41,18 +43,37 @@ impl Root {
|
||||
."justify-between"
|
||||
."items-center"
|
||||
{
|
||||
span ."text-xl" ."font-semibold" {
|
||||
span
|
||||
."text-xl"
|
||||
."font-semibold"
|
||||
."flex"
|
||||
."flex-row"
|
||||
."items-center"
|
||||
."gap-3"
|
||||
{
|
||||
img ."h-12" src="/assets/luggage.svg";
|
||||
a href="/" { "Packager" }
|
||||
}
|
||||
nav ."grow" ."flex" ."flex-row" ."justify-center" ."gap-x-6" {
|
||||
a href="/inventory/" class={@match active_page {
|
||||
TopLevelPage::Inventory => "text-lg font-bold underline",
|
||||
_ => "text-lg",
|
||||
}} { "Inventory" }
|
||||
a href="/trips/" class={@match active_page {
|
||||
TopLevelPage::Trips => "text-lg font-bold underline",
|
||||
_ => "text-lg",
|
||||
}} { "Trips" }
|
||||
nav
|
||||
."grow"
|
||||
."flex"
|
||||
."flex-row"
|
||||
."justify-center"
|
||||
."gap-x-10"
|
||||
."content-stretch"
|
||||
{
|
||||
a href="/inventory/"
|
||||
."h-full"
|
||||
."text-lg"
|
||||
."font-bold"[matches!(active_page, TopLevelPage::Inventory)]
|
||||
."underline"[matches!(active_page, TopLevelPage::Inventory)]
|
||||
{ "Inventory" }
|
||||
a href="/trips/"
|
||||
."h-full"
|
||||
."text-lg"
|
||||
."font-bold"[matches!(active_page, TopLevelPage::Trips)]
|
||||
."underline"[matches!(active_page, TopLevelPage::Trips)]
|
||||
{ "Trips" }
|
||||
}
|
||||
}
|
||||
(body)
|
||||
|
||||
@@ -280,6 +280,7 @@ impl TripInfoRow {
|
||||
attribute_key: TripAttribute,
|
||||
edit_attribute: Option<&TripAttribute>,
|
||||
input_type: InputType,
|
||||
has_two_columns: bool,
|
||||
) -> Markup {
|
||||
let edit = edit_attribute.map_or(false, |a| *a == attribute_key);
|
||||
html!(
|
||||
@@ -357,6 +358,7 @@ impl TripInfoRow {
|
||||
td ."border" ."p-2" { (name) }
|
||||
td ."border" ."p-2" { (value.map_or("".to_string(), |v| v.to_string())) }
|
||||
td
|
||||
colspan=(if has_two_columns {"2"} else {"1"})
|
||||
."border-none"
|
||||
."bg-blue-100"
|
||||
."hover:bg-blue-200"
|
||||
@@ -387,6 +389,8 @@ pub struct TripInfo;
|
||||
|
||||
impl TripInfo {
|
||||
pub fn build(state: &ClientState, trip: &models::Trip) -> Markup {
|
||||
let has_two_columns =
|
||||
state.trip_edit_attribute.is_some() || !(trip.state.is_first() || trip.state.is_last());
|
||||
html!(
|
||||
table
|
||||
."table"
|
||||
@@ -397,97 +401,216 @@ impl TripInfo {
|
||||
."w-full"
|
||||
{
|
||||
tbody {
|
||||
(TripInfoRow::build("Location", trip.location.as_ref(), TripAttribute::Location, state.trip_edit_attribute.as_ref(), InputType::Text))
|
||||
(TripInfoRow::build("Start date", Some(trip.date_start), TripAttribute::DateStart, state.trip_edit_attribute.as_ref(), InputType::Date))
|
||||
(TripInfoRow::build("End date", Some(trip.date_end), TripAttribute::DateEnd, state.trip_edit_attribute.as_ref(), InputType::Date))
|
||||
(TripInfoRow::build("Temp (min)", trip.temp_min, TripAttribute::TempMin, state.trip_edit_attribute.as_ref(), InputType::Number))
|
||||
(TripInfoRow::build("Temp (max)", trip.temp_max, TripAttribute::TempMax, state.trip_edit_attribute.as_ref(), InputType::Number))
|
||||
(TripInfoRow::build("Location",
|
||||
trip.location.as_ref(),
|
||||
TripAttribute::Location,
|
||||
state.trip_edit_attribute.as_ref(),
|
||||
InputType::Text,
|
||||
has_two_columns
|
||||
))
|
||||
(TripInfoRow::build("Start date",
|
||||
Some(trip.date_start),
|
||||
TripAttribute::DateStart,
|
||||
state.trip_edit_attribute.as_ref(),
|
||||
InputType::Date,
|
||||
has_two_columns
|
||||
))
|
||||
(TripInfoRow::build("End date",
|
||||
Some(trip.date_end),
|
||||
TripAttribute::DateEnd,
|
||||
state.trip_edit_attribute.as_ref(),
|
||||
InputType::Date,
|
||||
has_two_columns
|
||||
))
|
||||
(TripInfoRow::build("Temp (min)",
|
||||
trip.temp_min,
|
||||
TripAttribute::TempMin,
|
||||
state.trip_edit_attribute.as_ref(),
|
||||
InputType::Number,
|
||||
has_two_columns
|
||||
))
|
||||
(TripInfoRow::build("Temp (max)",
|
||||
trip.temp_max,
|
||||
TripAttribute::TempMax,
|
||||
state.trip_edit_attribute.as_ref(),
|
||||
InputType::Number,
|
||||
has_two_columns
|
||||
))
|
||||
tr .h-full {
|
||||
td ."border" ."p-2" { "Types" }
|
||||
td ."border" ."p-2" {
|
||||
ul
|
||||
."flex"
|
||||
."flex-row"
|
||||
."flex-wrap"
|
||||
."gap-2"
|
||||
."justify-between"
|
||||
// as we have a gap between the elements, we have
|
||||
// to completely skip an element when there are no
|
||||
// active or inactive items, otherwise we get the gap
|
||||
// between the empty (invisible) item, throwing off
|
||||
// the margins
|
||||
{
|
||||
@let types = trip.types();
|
||||
@let active_triptypes = types.iter().filter(|t| t.active).collect::<Vec<&TripType>>();
|
||||
@let inactive_triptypes = types.iter().filter(|t| !t.active).collect::<Vec<&TripType>>();
|
||||
td ."border" ."p-2" { "State" }
|
||||
td ."border" ."p-2" { (trip.state) }
|
||||
@let prev_state = trip.state.prev();
|
||||
@let next_state = trip.state.next();
|
||||
|
||||
@if !active_triptypes.is_empty() {
|
||||
div
|
||||
."flex"
|
||||
."flex-row"
|
||||
."flex-wrap"
|
||||
."gap-2"
|
||||
."justify-start"
|
||||
@if let Some(ref prev_state) = prev_state {
|
||||
td
|
||||
colspan=(if next_state.is_none() && has_two_columns { "2" } else { "1" })
|
||||
."border-none"
|
||||
."bg-yellow-100"
|
||||
."hover:bg-yellow-200"
|
||||
."p-0"
|
||||
."w-8"
|
||||
."h-full"
|
||||
{
|
||||
form
|
||||
action={"./state/" (prev_state)}
|
||||
method="post"
|
||||
."flex"
|
||||
."w-full"
|
||||
."h-full"
|
||||
|
||||
{
|
||||
button
|
||||
type="submit"
|
||||
."w-full"
|
||||
."h-full"
|
||||
{
|
||||
@for triptype in active_triptypes {
|
||||
a href=(format!("type/{}/remove", triptype.id)) {
|
||||
li
|
||||
."border"
|
||||
."rounded-2xl"
|
||||
."py-0.5"
|
||||
."px-2"
|
||||
."bg-green-100"
|
||||
."cursor-pointer"
|
||||
."flex"
|
||||
."flex-column"
|
||||
."items-center"
|
||||
."hover:bg-red-200"
|
||||
."gap-1"
|
||||
{
|
||||
span { (triptype.name) }
|
||||
span ."mdi" ."mdi-delete" ."text-sm" {}
|
||||
}
|
||||
}
|
||||
}
|
||||
span
|
||||
."m-auto"
|
||||
."mdi"
|
||||
."mdi-step-backward"
|
||||
."text-xl";
|
||||
}
|
||||
}
|
||||
@if !inactive_triptypes.is_empty() {
|
||||
div
|
||||
."flex"
|
||||
."flex-row"
|
||||
."flex-wrap"
|
||||
."gap-2"
|
||||
."justify-start"
|
||||
}
|
||||
}
|
||||
@if let Some(ref next_state) = trip.state.next() {
|
||||
td
|
||||
colspan=(if prev_state.is_none() && has_two_columns { "2" } else { "1" })
|
||||
."border-none"
|
||||
."bg-green-100"
|
||||
."hover:bg-green-200"
|
||||
."p-0"
|
||||
."w-8"
|
||||
."h-full"
|
||||
{
|
||||
form
|
||||
action={"./state/" (next_state)}
|
||||
method="post"
|
||||
."flex"
|
||||
."w-full"
|
||||
."h-full"
|
||||
|
||||
{
|
||||
button
|
||||
type="submit"
|
||||
."w-full"
|
||||
."h-full"
|
||||
{
|
||||
@for triptype in inactive_triptypes {
|
||||
a href=(format!("type/{}/add", triptype.id)) {
|
||||
li
|
||||
."border"
|
||||
."rounded-2xl"
|
||||
."py-0.5"
|
||||
."px-2"
|
||||
."bg-gray-100"
|
||||
."cursor-pointer"
|
||||
."flex"
|
||||
."flex-column"
|
||||
."items-center"
|
||||
."hover:bg-green-200"
|
||||
."gap-1"
|
||||
."opacity-60"
|
||||
{
|
||||
span { (triptype.name) }
|
||||
span ."mdi" ."mdi-plus" ."text-sm" {}
|
||||
}
|
||||
}
|
||||
}
|
||||
span
|
||||
."m-auto"
|
||||
."mdi"
|
||||
."mdi-step-forward"
|
||||
."text-xl";
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
tr .h-full {
|
||||
td ."border" ."p-2" { "Types" }
|
||||
td ."border" {
|
||||
div
|
||||
."flex"
|
||||
."flex-row"
|
||||
."items-center"
|
||||
."justify-between"
|
||||
{
|
||||
ul
|
||||
."flex"
|
||||
."flex-row"
|
||||
."flex-wrap"
|
||||
."gap-2"
|
||||
."justify-between"
|
||||
."p-2"
|
||||
// as we have a gap between the elements, we have
|
||||
// to completely skip an element when there are no
|
||||
// active or inactive items, otherwise we get the gap
|
||||
// between the empty (invisible) item, throwing off
|
||||
// the margins
|
||||
{
|
||||
@let types = trip.types();
|
||||
@let active_triptypes = types.iter().filter(|t| t.active).collect::<Vec<&TripType>>();
|
||||
@let inactive_triptypes = types.iter().filter(|t| !t.active).collect::<Vec<&TripType>>();
|
||||
|
||||
@if !active_triptypes.is_empty() {
|
||||
div
|
||||
."flex"
|
||||
."flex-row"
|
||||
."flex-wrap"
|
||||
."gap-2"
|
||||
."justify-start"
|
||||
{
|
||||
@for triptype in active_triptypes {
|
||||
a href=(format!("type/{}/remove", triptype.id)) {
|
||||
li
|
||||
."border"
|
||||
."rounded-2xl"
|
||||
."py-0.5"
|
||||
."px-2"
|
||||
."bg-green-100"
|
||||
."cursor-pointer"
|
||||
."flex"
|
||||
."flex-column"
|
||||
."items-center"
|
||||
."hover:bg-red-200"
|
||||
."gap-1"
|
||||
{
|
||||
span { (triptype.name) }
|
||||
span ."mdi" ."mdi-delete" ."text-sm" {}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@if !inactive_triptypes.is_empty() {
|
||||
div
|
||||
."flex"
|
||||
."flex-row"
|
||||
."flex-wrap"
|
||||
."gap-2"
|
||||
."justify-start"
|
||||
{
|
||||
@for triptype in inactive_triptypes {
|
||||
a href=(format!("type/{}/add", triptype.id)) {
|
||||
li
|
||||
."border"
|
||||
."rounded-2xl"
|
||||
."py-0.5"
|
||||
."px-2"
|
||||
."bg-gray-100"
|
||||
."cursor-pointer"
|
||||
."flex"
|
||||
."flex-column"
|
||||
."items-center"
|
||||
."hover:bg-green-200"
|
||||
."gap-1"
|
||||
."opacity-60"
|
||||
{
|
||||
span { (triptype.name) }
|
||||
span ."mdi" ."mdi-plus" ."text-sm" {}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
a href="/trips/types/"
|
||||
."text-sm"
|
||||
."text-gray-500"
|
||||
."mr-2"
|
||||
{
|
||||
"Manage" br; "types"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
tr .h-full {
|
||||
td ."border" ."p-2" { "Carried weight" }
|
||||
td ."border" ."p-2" { "TODO" }
|
||||
td ."border" ."p-2"
|
||||
{
|
||||
(trip.total_picked_weight())
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -615,6 +738,7 @@ impl TripCategoryList {
|
||||
}
|
||||
tbody {
|
||||
@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"
|
||||
@@ -624,29 +748,66 @@ impl TripCategoryList {
|
||||
."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
|
||||
|
||||
."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"
|
||||
."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" ."p-2" ."m-0" style="position:relative;" {
|
||||
td ."border" ."m-0" ."p-2" style="position:relative;" {
|
||||
p {
|
||||
(category.total_picked_weight().to_string())
|
||||
}
|
||||
@@ -762,14 +923,31 @@ impl TripItemList {
|
||||
}
|
||||
}
|
||||
td ."border" ."p-0" {
|
||||
a
|
||||
."p-2" ."w-full" ."inline-block"
|
||||
href=(
|
||||
format!("/inventory/item/{id}/", id=item.item.id)
|
||||
) {
|
||||
|
||||
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()) }
|
||||
|
||||
@@ -3,7 +3,7 @@ use axum::{
|
||||
extract::{Path, Query, State},
|
||||
headers,
|
||||
headers::Header,
|
||||
http::{header::HeaderMap, StatusCode},
|
||||
http::{header, header::HeaderMap, StatusCode},
|
||||
response::{Html, Redirect},
|
||||
routing::{get, post},
|
||||
Form, Router,
|
||||
@@ -89,13 +89,23 @@ async fn main() -> Result<(), sqlx::Error> {
|
||||
client_state: ClientState::new(),
|
||||
};
|
||||
|
||||
let icon_handler = || async {
|
||||
(
|
||||
[(header::CONTENT_TYPE, "image/svg+xml")],
|
||||
include_str!(concat!(env!("CARGO_MANIFEST_DIR"), "/assets/luggage.svg")),
|
||||
)
|
||||
};
|
||||
|
||||
// build our application with a route
|
||||
let app = Router::new()
|
||||
.route("/favicon.svg", get(icon_handler))
|
||||
.route("/assets/luggage.svg", get(icon_handler))
|
||||
.route("/", get(root))
|
||||
.route("/trips/", get(trips))
|
||||
.route("/trip/", post(trip_create))
|
||||
.route("/trip/:id/", get(trip))
|
||||
.route("/trip/:id/comment/submit", post(trip_comment_set))
|
||||
.route("/trip/:id/state/:id", post(trip_state_set))
|
||||
.route("/trip/:id/type/:id/add", get(trip_type_add))
|
||||
.route("/trip/:id/type/:id/remove", get(trip_type_remove))
|
||||
.route(
|
||||
@@ -469,6 +479,7 @@ async fn trip_create(
|
||||
.date_end
|
||||
.format(DATE_FORMAT)
|
||||
.map_err(|e| (StatusCode::BAD_REQUEST, e.to_string()))?;
|
||||
let trip_state = TripState::new();
|
||||
query!(
|
||||
"INSERT INTO trips
|
||||
(id, name, date_start, date_end, state)
|
||||
@@ -478,7 +489,7 @@ async fn trip_create(
|
||||
new_trip.name,
|
||||
date_start,
|
||||
date_end,
|
||||
TripState::Planning,
|
||||
trip_state
|
||||
)
|
||||
.execute(&state.database_pool)
|
||||
.await
|
||||
@@ -628,6 +639,15 @@ async fn trip(
|
||||
)
|
||||
})?;
|
||||
|
||||
trip.sync_trip_items_with_inventory(&state.database_pool)
|
||||
.await
|
||||
.map_err(|e| {
|
||||
(
|
||||
StatusCode::INTERNAL_SERVER_ERROR,
|
||||
ErrorPage::build(&e.to_string()),
|
||||
)
|
||||
})?;
|
||||
|
||||
trip.load_categories(&state.database_pool)
|
||||
.await
|
||||
.map_err(|e| {
|
||||
@@ -1013,3 +1033,29 @@ async fn inventory_category_create(
|
||||
|
||||
Ok(Redirect::to("/inventory/"))
|
||||
}
|
||||
|
||||
async fn trip_state_set(
|
||||
State(state): State<AppState>,
|
||||
Path((trip_id, new_state)): Path<(Uuid, TripState)>,
|
||||
) -> Result<Redirect, (StatusCode, Markup)> {
|
||||
let trip_id = trip_id.to_string();
|
||||
let result = query!(
|
||||
"UPDATE trips
|
||||
SET state = ?
|
||||
WHERE id = ?",
|
||||
new_state,
|
||||
trip_id,
|
||||
)
|
||||
.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 {id} not found", id = trip_id)),
|
||||
))
|
||||
} else {
|
||||
Ok(Redirect::to(&format!("/trip/{id}/", id = trip_id)))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -98,8 +98,9 @@ impl convert::From<TimeParseError> for Error {
|
||||
|
||||
impl error::Error for Error {}
|
||||
|
||||
#[derive(sqlx::Type)]
|
||||
#[derive(sqlx::Type, PartialEq, PartialOrd, Deserialize)]
|
||||
pub enum TripState {
|
||||
Init,
|
||||
Planning,
|
||||
Planned,
|
||||
Active,
|
||||
@@ -107,12 +108,49 @@ pub enum TripState {
|
||||
Done,
|
||||
}
|
||||
|
||||
impl TripState {
|
||||
pub fn new() -> Self {
|
||||
TripState::Init
|
||||
}
|
||||
|
||||
pub fn next(&self) -> Option<Self> {
|
||||
match self {
|
||||
Self::Init => Some(Self::Planning),
|
||||
Self::Planning => Some(Self::Planned),
|
||||
Self::Planned => Some(Self::Active),
|
||||
Self::Active => Some(Self::Review),
|
||||
Self::Review => Some(Self::Done),
|
||||
Self::Done => None,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn prev(&self) -> Option<Self> {
|
||||
match self {
|
||||
Self::Init => None,
|
||||
Self::Planning => Some(Self::Init),
|
||||
Self::Planned => Some(Self::Planning),
|
||||
Self::Active => Some(Self::Planned),
|
||||
Self::Review => Some(Self::Active),
|
||||
Self::Done => Some(Self::Review),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn is_first(&self) -> bool {
|
||||
self == &TripState::new()
|
||||
}
|
||||
|
||||
pub fn is_last(&self) -> bool {
|
||||
self == &TripState::Done
|
||||
}
|
||||
}
|
||||
|
||||
impl fmt::Display for TripState {
|
||||
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
|
||||
write!(
|
||||
f,
|
||||
"{}",
|
||||
match self {
|
||||
Self::Init => "Init",
|
||||
Self::Planning => "Planning",
|
||||
Self::Planned => "Planned",
|
||||
Self::Active => "Active",
|
||||
@@ -128,6 +166,7 @@ impl std::convert::TryFrom<&str> for TripState {
|
||||
|
||||
fn try_from(value: &str) -> Result<Self, Self::Error> {
|
||||
Ok(match value {
|
||||
"Init" => Self::Init,
|
||||
"Planning" => Self::Planning,
|
||||
"Planned" => Self::Planned,
|
||||
"Active" => Self::Active,
|
||||
@@ -184,6 +223,7 @@ pub struct TripItem {
|
||||
pub item: Item,
|
||||
pub picked: bool,
|
||||
pub packed: bool,
|
||||
pub new: bool,
|
||||
}
|
||||
|
||||
pub struct DbTripRow {
|
||||
@@ -326,6 +366,21 @@ impl<'a> Trip {
|
||||
}
|
||||
|
||||
impl<'a> Trip {
|
||||
pub fn total_picked_weight(&self) -> i64 {
|
||||
self.categories()
|
||||
.iter()
|
||||
.map(|category| -> i64 {
|
||||
category
|
||||
.items
|
||||
.as_ref()
|
||||
.unwrap()
|
||||
.iter()
|
||||
.filter_map(|item| Some(item.item.weight).filter(|_| item.picked))
|
||||
.sum::<i64>()
|
||||
})
|
||||
.sum::<i64>()
|
||||
}
|
||||
|
||||
pub async fn load_trips_types(
|
||||
&'a mut self,
|
||||
pool: &sqlx::Pool<sqlx::Sqlite>,
|
||||
@@ -372,6 +427,79 @@ impl<'a> Trip {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub async fn sync_trip_items_with_inventory(
|
||||
&'a mut self,
|
||||
pool: &sqlx::Pool<sqlx::Sqlite>,
|
||||
) -> Result<(), Error> {
|
||||
// we need to get all items that are part of the inventory but not
|
||||
// part of the trip items
|
||||
//
|
||||
// then, we know which items we need to sync. there are different
|
||||
// states for them:
|
||||
//
|
||||
// * if the trip is new (it's state is INITIAL), we can just forward
|
||||
// as-is
|
||||
// * if the trip is new, we have to make these new items prominently
|
||||
// visible so the user knows that there might be new items to
|
||||
// consider
|
||||
let trip_id = self.id.to_string();
|
||||
let unsynced_items: Vec<Uuid> = sqlx::query!(
|
||||
"
|
||||
SELECT
|
||||
i_item.id AS item_id
|
||||
FROM inventory_items AS i_item
|
||||
LEFT JOIN (
|
||||
SELECT t_item.item_id as item_id
|
||||
FROM trips_items AS t_item
|
||||
WHERE t_item.trip_id = ?
|
||||
) AS t_item
|
||||
ON t_item.item_id = i_item.id
|
||||
WHERE t_item.item_id IS NULL
|
||||
",
|
||||
trip_id
|
||||
)
|
||||
.fetch(pool)
|
||||
.map_ok(|row| -> Result<Uuid, Error> { Ok(Uuid::try_parse(&row.item_id)?) })
|
||||
.try_collect::<Vec<Result<Uuid, Error>>>()
|
||||
.await?
|
||||
.into_iter()
|
||||
.collect::<Result<Vec<Uuid>, Error>>()?;
|
||||
|
||||
// looks like there is currently no nice way to do multiple inserts
|
||||
// with sqlx. whatever, this won't matter
|
||||
|
||||
// only mark as new when the trip is already underway
|
||||
let mark_as_new = self.state != TripState::new();
|
||||
|
||||
for unsynced_item in &unsynced_items {
|
||||
let item_id = unsynced_item.to_string();
|
||||
sqlx::query!(
|
||||
"
|
||||
INSERT INTO trips_items
|
||||
(
|
||||
item_id,
|
||||
trip_id,
|
||||
pick,
|
||||
pack,
|
||||
new
|
||||
)
|
||||
VALUES (?, ?, ?, ?, ?)
|
||||
",
|
||||
item_id,
|
||||
trip_id,
|
||||
false,
|
||||
false,
|
||||
mark_as_new,
|
||||
)
|
||||
.execute(pool)
|
||||
.await?;
|
||||
}
|
||||
|
||||
tracing::info!("unsynced items: {:?}", &unsynced_items);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub async fn load_categories(
|
||||
&'a mut self,
|
||||
pool: &sqlx::Pool<sqlx::Sqlite>,
|
||||
@@ -392,7 +520,8 @@ impl<'a> Trip {
|
||||
inner.item_description AS item_description,
|
||||
inner.item_weight AS item_weight,
|
||||
inner.item_is_picked AS item_is_picked,
|
||||
inner.item_is_packed AS item_is_packed
|
||||
inner.item_is_packed AS item_is_packed,
|
||||
inner.item_is_new AS item_is_new
|
||||
FROM inventory_items_categories AS category
|
||||
LEFT JOIN (
|
||||
SELECT
|
||||
@@ -405,7 +534,8 @@ impl<'a> Trip {
|
||||
item.description as item_description,
|
||||
item.weight as item_weight,
|
||||
trip.pick as item_is_picked,
|
||||
trip.pack as item_is_packed
|
||||
trip.pack as item_is_packed,
|
||||
trip.new as item_is_new
|
||||
FROM trips_items as trip
|
||||
INNER JOIN inventory_items as item
|
||||
ON item.id = trip.item_id
|
||||
@@ -423,8 +553,6 @@ impl<'a> Trip {
|
||||
category: Category {
|
||||
id: Uuid::try_parse(&row.category_id)?,
|
||||
name: row.category_name,
|
||||
// TODO align optionality between code and database
|
||||
// idea: make description nullable
|
||||
description: row.category_description,
|
||||
|
||||
items: None,
|
||||
@@ -450,6 +578,7 @@ impl<'a> Trip {
|
||||
},
|
||||
picked: row.item_is_picked.unwrap(),
|
||||
packed: row.item_is_packed.unwrap(),
|
||||
new: row.item_is_new.unwrap(),
|
||||
};
|
||||
|
||||
if let Some(&mut ref mut c) = categories
|
||||
|
||||
Reference in New Issue
Block a user