From 2eca0ae3c63caa2ddc6725ac71c24824fb3cd4fb Mon Sep 17 00:00:00 2001 From: Jon Seager Date: Wed, 4 Feb 2026 20:53:12 +0000 Subject: [PATCH] refactor(bags): replace dual-section bag list with single paginated table Remove the open-bags card grid and closed-bags history table in favour of a unified table showing all bags with status badges and actions. - Simplify load_bag_page to single BagFilter::all() query - Remove open_bags field from BagPageData and BagListTemplate - Add Status column with Open/Closed badges and remaining weight - Add Close and Delete action buttons per row --- src/application/routes/bags.rs | 28 +----- src/application/routes/data.rs | 1 - src/presentation/web/templates.rs | 1 - templates/partials/bag_list.html | 141 +++++++++++++++++++----------- 4 files changed, 93 insertions(+), 78 deletions(-) diff --git a/src/application/routes/bags.rs b/src/application/routes/bags.rs index 273c3a0..275b437 100644 --- a/src/application/routes/bags.rs +++ b/src/application/routes/bags.rs @@ -22,7 +22,6 @@ const BAG_PAGE_PATH: &str = "/data?type=bags"; const BAG_FRAGMENT_PATH: &str = "/data?type=bags#bag-list"; pub(super) struct BagPageData { - pub(super) open_bags: Vec, pub(super) bags: Paginated, pub(super) navigator: ListNavigator, } @@ -33,21 +32,9 @@ pub(super) async fn load_bag_page( request: ListRequest, search: Option<&str>, ) -> Result { - let open_request = ListRequest::show_all(BagSortKey::RoastDate, SortDirection::Desc); - let open_page = state - .bag_repo - .list(BagFilter::open(), &open_request, None) - .await - .map_err(AppError::from)?; - let open_bags_view = open_page - .items - .into_iter() - .map(BagView::from_domain) - .collect(); - let page = state .bag_repo - .list(BagFilter::closed(), &request, search) + .list(BagFilter::all(), &request, search) .await .map_err(AppError::from)?; @@ -60,11 +47,7 @@ pub(super) async fn load_bag_page( search.map(String::from), ); - Ok(BagPageData { - open_bags: open_bags_view, - bags, - navigator, - }) + Ok(BagPageData { bags, navigator }) } #[tracing::instrument(skip(state, _auth_user, headers, query))] @@ -295,15 +278,10 @@ async fn render_bag_list_fragment( search: Option, is_authenticated: bool, ) -> Result { - let BagPageData { - open_bags, - bags, - navigator, - } = load_bag_page(&state, request, search.as_deref()).await?; + let BagPageData { bags, navigator } = load_bag_page(&state, request, search.as_deref()).await?; let template = BagListTemplate { is_authenticated, - open_bags, bags, navigator, }; diff --git a/src/application/routes/data.rs b/src/application/routes/data.rs index 15d6b4c..803fa8b 100644 --- a/src/application/routes/data.rs +++ b/src/application/routes/data.rs @@ -198,7 +198,6 @@ async fn render_bags( render_list( BagListTemplate { is_authenticated, - open_bags: data.open_bags, bags: data.bags, navigator: data.navigator, }, diff --git a/src/presentation/web/templates.rs b/src/presentation/web/templates.rs index ec7701c..c53be96 100644 --- a/src/presentation/web/templates.rs +++ b/src/presentation/web/templates.rs @@ -54,7 +54,6 @@ pub struct TimelineChunkTemplate { #[template(path = "partials/bag_list.html")] pub struct BagListTemplate { pub is_authenticated: bool, - pub open_bags: Vec, pub bags: Paginated, pub navigator: ListNavigator, } diff --git a/templates/partials/bag_list.html b/templates/partials/bag_list.html index e013b7b..db86205 100644 --- a/templates/partials/bag_list.html +++ b/templates/partials/bag_list.html @@ -1,57 +1,96 @@ {% import "partials/table.html" as table %} -{% import "partials/bag_card.html" as bag_card %} +{% import "partials/icons.html" as icons %} -
- {% if !open_bags.is_empty() %} -
-

Open Bags

-
- {% for bag in open_bags %} - {% call bag_card::card(bag, is_authenticated) %} - {% endfor %} +
+ {% if bags.items.is_empty() && !navigator.has_search() %} +
+

+ No bags recorded yet. Use the form above to add your first bag. +

+
+ {% else %} +
+ {% call table::search_header(navigator, "#bag-list") %} +
+ + + + {% call table::sortable_header("Added", "created-at", navigator, "#bag-list") %} + {% call table::sortable_header("Roaster", "roaster", navigator, "#bag-list") %} + {% call table::sortable_header("Roast", "roast", navigator, "#bag-list") %} + + + {% call table::sortable_header("Finished", "finished-at", navigator, "#bag-list") %} + {% if is_authenticated %} + + {% endif %} + + + + {% for bag in bags.items %} + + + + + + + {% if !bag.closed %} + + {% endif %} + + {% if is_authenticated %} + + {% endif %} + + {% endfor %} + +
WeightStatusActions
+ {{ bag.created_at }} + + {{ bag.roaster_name }} + {{ bag.roast_name }}{{ bag.amount }}g + {% if bag.closed %} + Closed + {% else %} + Open + + {% endif %} + {{ bag.remaining }}g left{{ bag.finished_at }} +
+ {% if !bag.closed %} + + {% endif %} + +
+
+ {% if bags.items.is_empty() %} +
No bags match your search.
+ {% endif %} + {% call table::pagination_header(bags, navigator, "#bag-list") %} + {% if bags.has_next() %} + + {% endif %}
{% endif %} - -
-

History

-
- {% call table::search_header(navigator, "#bag-list") %} - -
- - - - {% call table::sortable_header("Added", "created-at", navigator, "#bag-list") %} - {% call table::sortable_header("Roaster", "roaster", navigator, "#bag-list") %} - {% call table::sortable_header("Roast", "roast", navigator, "#bag-list") %} - - {% call table::sortable_header("Finished", "finished-at", navigator, "#bag-list") %} - - - - {% for bag in bags.items %} - - - - - - - - {% endfor %} - -
Weight
- {{ bag.created_at }} - - {{ bag.roaster_name }} - {{ bag.roast_name }}{{ bag.amount }}g{{ bag.finished_at }}
-
- {% call table::pagination_header(bags, navigator, "#bag-list") %} - {% if bags.has_next() %} - - {% endif %} -
-