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
This commit is contained in:
parent
4bdfc661f9
commit
2eca0ae3c6
4 changed files with 93 additions and 78 deletions
|
|
@ -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<BagView>,
|
||||
pub(super) bags: Paginated<BagView>,
|
||||
pub(super) navigator: ListNavigator<BagSortKey>,
|
||||
}
|
||||
|
|
@ -33,21 +32,9 @@ pub(super) async fn load_bag_page(
|
|||
request: ListRequest<BagSortKey>,
|
||||
search: Option<&str>,
|
||||
) -> Result<BagPageData, AppError> {
|
||||
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<String>,
|
||||
is_authenticated: bool,
|
||||
) -> Result<Response, AppError> {
|
||||
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,
|
||||
};
|
||||
|
|
|
|||
|
|
@ -198,7 +198,6 @@ async fn render_bags(
|
|||
render_list(
|
||||
BagListTemplate {
|
||||
is_authenticated,
|
||||
open_bags: data.open_bags,
|
||||
bags: data.bags,
|
||||
navigator: data.navigator,
|
||||
},
|
||||
|
|
|
|||
|
|
@ -54,7 +54,6 @@ pub struct TimelineChunkTemplate {
|
|||
#[template(path = "partials/bag_list.html")]
|
||||
pub struct BagListTemplate {
|
||||
pub is_authenticated: bool,
|
||||
pub open_bags: Vec<BagView>,
|
||||
pub bags: Paginated<BagView>,
|
||||
pub navigator: ListNavigator<BagSortKey>,
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,25 +1,20 @@
|
|||
{% import "partials/table.html" as table %}
|
||||
{% import "partials/bag_card.html" as bag_card %}
|
||||
{% import "partials/icons.html" as icons %}
|
||||
|
||||
<div id="bag-list" class="mt-6 space-y-8" data-star-scope="bags">
|
||||
{% if !open_bags.is_empty() %}
|
||||
<section>
|
||||
<h2 class="text-lg font-semibold text-amber-700 mb-3">Open Bags</h2>
|
||||
<div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4">
|
||||
{% for bag in open_bags %}
|
||||
{% call bag_card::card(bag, is_authenticated) %}
|
||||
{% endfor %}
|
||||
<div id="bag-list" class="mt-6" data-star-scope="bags">
|
||||
{% if bags.items.is_empty() && !navigator.has_search() %}
|
||||
<div
|
||||
class="rounded-lg border border-dashed border-amber-300 bg-amber-100/40 px-4 py-6 text-sm text-stone-600"
|
||||
>
|
||||
<p class="text-center">
|
||||
No bags recorded yet. Use the form above to add your first bag.
|
||||
</p>
|
||||
</div>
|
||||
</section>
|
||||
{% endif %}
|
||||
|
||||
<section>
|
||||
<h2 class="text-2xl font-bold mb-4 text-stone-800">History</h2>
|
||||
<div class="rounded-lg border border-amber-300 bg-amber-100/80 shadow-sm"
|
||||
{% else %}
|
||||
<section class="rounded-lg border border-amber-300 bg-amber-100/80 shadow-sm"
|
||||
{% if bags.has_next() %}data-infinite-scroll data-next-url="{{ navigator.fragment_page_href(bags.next_page().unwrap())|safe }}" data-target="#bag-list"{% endif %}
|
||||
>
|
||||
{% call table::search_header(navigator, "#bag-list") %}
|
||||
|
||||
<div class="overflow-x-auto">
|
||||
<table class="responsive-table min-w-full divide-y divide-amber-200 text-left text-sm text-stone-700">
|
||||
<thead class="bg-amber-200/60 text-xs font-semibold tracking-wide text-amber-900">
|
||||
|
|
@ -28,7 +23,11 @@
|
|||
{% call table::sortable_header("Roaster", "roaster", navigator, "#bag-list") %}
|
||||
{% call table::sortable_header("Roast", "roast", navigator, "#bag-list") %}
|
||||
<th scope="col" class="px-4 py-3">Weight</th>
|
||||
<th scope="col" class="px-4 py-3">Status</th>
|
||||
{% call table::sortable_header("Finished", "finished-at", navigator, "#bag-list") %}
|
||||
{% if is_authenticated %}
|
||||
<th scope="col" class="px-4 py-3 text-right">Actions</th>
|
||||
{% endif %}
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody class="divide-y divide-amber-200/70">
|
||||
|
|
@ -42,16 +41,56 @@
|
|||
</td>
|
||||
<td data-label="Roast" class="px-4 py-3 whitespace-nowrap">{{ bag.roast_name }}</td>
|
||||
<td data-label="Weight" class="px-4 py-3 whitespace-nowrap">{{ bag.amount }}g</td>
|
||||
<td data-label="Status" class="px-4 py-3 whitespace-nowrap">
|
||||
{% if bag.closed %}
|
||||
<span class="inline-flex rounded-full bg-stone-100 px-2 py-1 text-xs font-medium text-stone-600">Closed</span>
|
||||
{% else %}
|
||||
<span class="inline-flex rounded-full bg-emerald-100 px-2 py-1 text-xs font-medium text-emerald-700">Open</span>
|
||||
<div class="hidden md:block text-xs text-stone-500">{{ bag.remaining }}g left</div>
|
||||
{% endif %}
|
||||
</td>
|
||||
{% if !bag.closed %}
|
||||
<td data-label="Remaining" class="px-4 py-3 whitespace-nowrap md:hidden">{{ bag.remaining }}g left</td>
|
||||
{% endif %}
|
||||
<td data-label="Finished" class="px-4 py-3 whitespace-nowrap">{{ bag.finished_at }}</td>
|
||||
{% if is_authenticated %}
|
||||
<td data-label="" class="px-4 py-3 text-right">
|
||||
<div class="inline-flex items-center gap-1">
|
||||
{% if !bag.closed %}
|
||||
<button
|
||||
type="button"
|
||||
class="inline-flex h-8 w-8 items-center justify-center rounded-md text-stone-500 transition hover:text-amber-700 focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-amber-500"
|
||||
title="Close bag"
|
||||
data-on:click="confirm('Close this bag? This will mark it as finished.') && @put('/api/v1/bags/{{ bag.id }}?closed=true&{{ navigator.query() }}', {responseOverrides: {selector: '#bag-list', mode: 'replace'}})"
|
||||
>
|
||||
<span class="sr-only">Close bag</span>
|
||||
{% call icons::x_circle("h-4 w-4") %}
|
||||
</button>
|
||||
{% endif %}
|
||||
<button
|
||||
type="button"
|
||||
class="inline-flex h-8 w-8 items-center justify-center rounded-md text-stone-500 transition hover:text-red-500 focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-red-500"
|
||||
title="Delete bag"
|
||||
data-on:click="confirm('Delete this bag?') && @delete('/api/v1/bags/{{ bag.id }}?{{ navigator.query() }}', {responseOverrides: {selector: '#bag-list', mode: 'replace'}})"
|
||||
>
|
||||
<span class="sr-only">Delete</span>
|
||||
{% call icons::delete("h-4 w-4") %}
|
||||
</button>
|
||||
</div>
|
||||
</td>
|
||||
{% endif %}
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
{% if bags.items.is_empty() %}
|
||||
<div class="p-8 text-center text-stone-500">No bags match your search.</div>
|
||||
{% endif %}
|
||||
{% call table::pagination_header(bags, navigator, "#bag-list") %}
|
||||
{% if bags.has_next() %}
|
||||
<div class="infinite-scroll-sentinel h-4 md:hidden" aria-hidden="true"></div>
|
||||
{% endif %}
|
||||
</div>
|
||||
</section>
|
||||
{% endif %}
|
||||
</div>
|
||||
|
|
|
|||
Loading…
Reference in a new issue