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";
|
const BAG_FRAGMENT_PATH: &str = "/data?type=bags#bag-list";
|
||||||
|
|
||||||
pub(super) struct BagPageData {
|
pub(super) struct BagPageData {
|
||||||
pub(super) open_bags: Vec<BagView>,
|
|
||||||
pub(super) bags: Paginated<BagView>,
|
pub(super) bags: Paginated<BagView>,
|
||||||
pub(super) navigator: ListNavigator<BagSortKey>,
|
pub(super) navigator: ListNavigator<BagSortKey>,
|
||||||
}
|
}
|
||||||
|
|
@ -33,21 +32,9 @@ pub(super) async fn load_bag_page(
|
||||||
request: ListRequest<BagSortKey>,
|
request: ListRequest<BagSortKey>,
|
||||||
search: Option<&str>,
|
search: Option<&str>,
|
||||||
) -> Result<BagPageData, AppError> {
|
) -> 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
|
let page = state
|
||||||
.bag_repo
|
.bag_repo
|
||||||
.list(BagFilter::closed(), &request, search)
|
.list(BagFilter::all(), &request, search)
|
||||||
.await
|
.await
|
||||||
.map_err(AppError::from)?;
|
.map_err(AppError::from)?;
|
||||||
|
|
||||||
|
|
@ -60,11 +47,7 @@ pub(super) async fn load_bag_page(
|
||||||
search.map(String::from),
|
search.map(String::from),
|
||||||
);
|
);
|
||||||
|
|
||||||
Ok(BagPageData {
|
Ok(BagPageData { bags, navigator })
|
||||||
open_bags: open_bags_view,
|
|
||||||
bags,
|
|
||||||
navigator,
|
|
||||||
})
|
|
||||||
}
|
}
|
||||||
|
|
||||||
#[tracing::instrument(skip(state, _auth_user, headers, query))]
|
#[tracing::instrument(skip(state, _auth_user, headers, query))]
|
||||||
|
|
@ -295,15 +278,10 @@ async fn render_bag_list_fragment(
|
||||||
search: Option<String>,
|
search: Option<String>,
|
||||||
is_authenticated: bool,
|
is_authenticated: bool,
|
||||||
) -> Result<Response, AppError> {
|
) -> Result<Response, AppError> {
|
||||||
let BagPageData {
|
let BagPageData { bags, navigator } = load_bag_page(&state, request, search.as_deref()).await?;
|
||||||
open_bags,
|
|
||||||
bags,
|
|
||||||
navigator,
|
|
||||||
} = load_bag_page(&state, request, search.as_deref()).await?;
|
|
||||||
|
|
||||||
let template = BagListTemplate {
|
let template = BagListTemplate {
|
||||||
is_authenticated,
|
is_authenticated,
|
||||||
open_bags,
|
|
||||||
bags,
|
bags,
|
||||||
navigator,
|
navigator,
|
||||||
};
|
};
|
||||||
|
|
|
||||||
|
|
@ -198,7 +198,6 @@ async fn render_bags(
|
||||||
render_list(
|
render_list(
|
||||||
BagListTemplate {
|
BagListTemplate {
|
||||||
is_authenticated,
|
is_authenticated,
|
||||||
open_bags: data.open_bags,
|
|
||||||
bags: data.bags,
|
bags: data.bags,
|
||||||
navigator: data.navigator,
|
navigator: data.navigator,
|
||||||
},
|
},
|
||||||
|
|
|
||||||
|
|
@ -54,7 +54,6 @@ pub struct TimelineChunkTemplate {
|
||||||
#[template(path = "partials/bag_list.html")]
|
#[template(path = "partials/bag_list.html")]
|
||||||
pub struct BagListTemplate {
|
pub struct BagListTemplate {
|
||||||
pub is_authenticated: bool,
|
pub is_authenticated: bool,
|
||||||
pub open_bags: Vec<BagView>,
|
|
||||||
pub bags: Paginated<BagView>,
|
pub bags: Paginated<BagView>,
|
||||||
pub navigator: ListNavigator<BagSortKey>,
|
pub navigator: ListNavigator<BagSortKey>,
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,25 +1,20 @@
|
||||||
{% import "partials/table.html" as table %}
|
{% 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">
|
<div id="bag-list" class="mt-6" data-star-scope="bags">
|
||||||
{% if !open_bags.is_empty() %}
|
{% if bags.items.is_empty() && !navigator.has_search() %}
|
||||||
<section>
|
<div
|
||||||
<h2 class="text-lg font-semibold text-amber-700 mb-3">Open Bags</h2>
|
class="rounded-lg border border-dashed border-amber-300 bg-amber-100/40 px-4 py-6 text-sm text-stone-600"
|
||||||
<div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4">
|
>
|
||||||
{% for bag in open_bags %}
|
<p class="text-center">
|
||||||
{% call bag_card::card(bag, is_authenticated) %}
|
No bags recorded yet. Use the form above to add your first bag.
|
||||||
{% endfor %}
|
</p>
|
||||||
</div>
|
</div>
|
||||||
</section>
|
{% else %}
|
||||||
{% endif %}
|
<section class="rounded-lg border border-amber-300 bg-amber-100/80 shadow-sm"
|
||||||
|
|
||||||
<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"
|
|
||||||
{% if bags.has_next() %}data-infinite-scroll data-next-url="{{ navigator.fragment_page_href(bags.next_page().unwrap())|safe }}" data-target="#bag-list"{% endif %}
|
{% 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") %}
|
{% call table::search_header(navigator, "#bag-list") %}
|
||||||
|
|
||||||
<div class="overflow-x-auto">
|
<div class="overflow-x-auto">
|
||||||
<table class="responsive-table min-w-full divide-y divide-amber-200 text-left text-sm text-stone-700">
|
<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">
|
<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("Roaster", "roaster", navigator, "#bag-list") %}
|
||||||
{% call table::sortable_header("Roast", "roast", 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">Weight</th>
|
||||||
|
<th scope="col" class="px-4 py-3">Status</th>
|
||||||
{% call table::sortable_header("Finished", "finished-at", navigator, "#bag-list") %}
|
{% 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>
|
</tr>
|
||||||
</thead>
|
</thead>
|
||||||
<tbody class="divide-y divide-amber-200/70">
|
<tbody class="divide-y divide-amber-200/70">
|
||||||
|
|
@ -42,16 +41,56 @@
|
||||||
</td>
|
</td>
|
||||||
<td data-label="Roast" class="px-4 py-3 whitespace-nowrap">{{ bag.roast_name }}</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="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>
|
<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>
|
</tr>
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
</tbody>
|
</tbody>
|
||||||
</table>
|
</table>
|
||||||
</div>
|
</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") %}
|
{% call table::pagination_header(bags, navigator, "#bag-list") %}
|
||||||
{% if bags.has_next() %}
|
{% if bags.has_next() %}
|
||||||
<div class="infinite-scroll-sentinel h-4 md:hidden" aria-hidden="true"></div>
|
<div class="infinite-scroll-sentinel h-4 md:hidden" aria-hidden="true"></div>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
</div>
|
|
||||||
</section>
|
</section>
|
||||||
|
{% endif %}
|
||||||
</div>
|
</div>
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue