This commit is contained in:
2023-08-29 21:33:59 +02:00
parent 7f80b83809
commit 3f834cd7d2
11 changed files with 708 additions and 275 deletions

View File

@@ -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" }

View File

@@ -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)

View File

@@ -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()) }