feat(ui): add usage progress bars to bag cards and data table

- Add used_percent field to BagView computed from amount/remaining
- Display accent-colored progress bar on homepage bag cards
- Replace Open pill with progress bar in bag data table (desktop and mobile)
- Merge Weight column into Status column showing remaining/total
- Add full-width progress bar to mobile card view for open bags
- Sort Status column by remaining grams (closed bags treated as 10000g)
- Hide Finished field on mobile for open bags
- Use small-caps typography for remaining text labels
This commit is contained in:
Jon Seager 2026-02-06 16:08:41 +00:00
parent 3a960b9d14
commit dcaac1ee9c
No known key found for this signature in database
5 changed files with 47 additions and 15 deletions

View file

@ -42,7 +42,9 @@ impl SqlBagRepository {
BagSortKey::UpdatedAt => format!("b.updated_at {dir_sql}, b.id DESC"), BagSortKey::UpdatedAt => format!("b.updated_at {dir_sql}, b.id DESC"),
BagSortKey::Roaster => format!("LOWER(rr.name) {dir_sql}, b.created_at DESC"), BagSortKey::Roaster => format!("LOWER(rr.name) {dir_sql}, b.created_at DESC"),
BagSortKey::Roast => format!("LOWER(r.name) {dir_sql}, b.created_at DESC"), BagSortKey::Roast => format!("LOWER(r.name) {dir_sql}, b.created_at DESC"),
BagSortKey::Status => format!("b.closed {dir_sql}, b.created_at DESC"), BagSortKey::Status => format!(
"CASE WHEN b.closed THEN 10000 ELSE b.remaining END {dir_sql}, b.created_at DESC"
),
BagSortKey::FinishedAt => format!("b.finished_at {dir_sql}, b.created_at DESC"), BagSortKey::FinishedAt => format!("b.finished_at {dir_sql}, b.created_at DESC"),
} }
} }

View file

@ -15,10 +15,17 @@ pub struct BagView {
pub roaster_name: String, pub roaster_name: String,
pub roast_slug: String, pub roast_slug: String,
pub roaster_slug: String, pub roaster_slug: String,
pub used_percent: u8,
} }
impl BagView { impl BagView {
pub fn from_domain(bag: BagWithRoast) -> Self { pub fn from_domain(bag: BagWithRoast) -> Self {
let used_percent = if bag.bag.amount > 0.0 {
(((bag.bag.amount - bag.bag.remaining) / bag.bag.amount) * 100.0).clamp(0.0, 100.0)
as u8
} else {
0
};
Self { Self {
id: bag.bag.id.to_string(), id: bag.bag.id.to_string(),
roast_id: bag.bag.roast_id.to_string(), roast_id: bag.bag.roast_id.to_string(),
@ -36,6 +43,7 @@ impl BagView {
roaster_name: bag.roaster_name, roaster_name: bag.roaster_name,
roast_slug: bag.roast_slug, roast_slug: bag.roast_slug,
roaster_slug: bag.roaster_slug, roaster_slug: bag.roaster_slug,
used_percent,
} }
} }
} }

View file

@ -445,6 +445,18 @@ a {
display: none; display: none;
} }
/* Progress bar: full-width above footer */
.responsive-table td.card-progress {
order: 98;
display: block;
margin: 0.25rem 0 0 !important;
padding: 0 !important;
}
.responsive-table td.card-progress::before {
display: none;
}
.pagination-controls { .pagination-controls {
display: none; display: none;
} }

View file

@ -5,10 +5,10 @@
<span class="block truncate font-semibold text-text">{{ bag.roast_name }}</span> <span class="block truncate font-semibold text-text">{{ bag.roast_name }}</span>
<p class="truncate text-sm text-text-muted">{{ bag.roaster_name }}</p> <p class="truncate text-sm text-text-muted">{{ bag.roaster_name }}</p>
</div> </div>
<p class="mt-2 truncate text-sm text-text-secondary"> <div class="mt-2 h-1.5 rounded-full bg-surface-alt overflow-hidden">
<span class="font-medium">{{ bag.remaining }}g</span> <div class="h-full rounded-full bg-accent" style="width: {{ bag.used_percent }}%"></div>
<span class="text-text-muted">of {{ bag.amount }}g remaining</span> </div>
</p> <p class="mt-1 truncate text-text-muted" style="font-variant: small-caps; font-size: 0.7rem">{{ bag.remaining }} / {{ bag.amount }}g</p>
{% if is_authenticated %} {% if is_authenticated %}
<div class="mt-3 pt-3 border-t flex gap-3"> <div class="mt-3 pt-3 border-t flex gap-3">
<a href="/add?type=brew&bag_id={{ bag.id }}" class="inline-flex items-center gap-1 text-sm font-medium text-accent hover:text-accent-hover"> <a href="/add?type=brew&bag_id={{ bag.id }}" class="inline-flex items-center gap-1 text-sm font-medium text-accent hover:text-accent-hover">

View file

@ -26,7 +26,6 @@
{% call table::sortable_header("Added", "created-at", 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("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>
{% call table::sortable_header("Status", "status", navigator, "#bag-list") %} {% call table::sortable_header("Status", "status", navigator, "#bag-list") %}
{% call table::sortable_header("Finished", "finished-at", navigator, "#bag-list") %} {% call table::sortable_header("Finished", "finished-at", navigator, "#bag-list") %}
{% if is_authenticated %} {% if is_authenticated %}
@ -49,21 +48,34 @@
<td data-label="Roast" class="card-title px-4 py-3 whitespace-nowrap"> <td data-label="Roast" class="card-title px-4 py-3 whitespace-nowrap">
{{ bag.roast_name }} {{ bag.roast_name }}
</td> </td>
<td data-label="Weight" class="px-4 py-3 whitespace-nowrap text-xs font-medium text-text-secondary">{{ bag.amount }}g</td> <td data-label="Status" class="mobile-hidden px-4 py-3 whitespace-nowrap">
<td data-label="Status" class="mobile-hidden px-4 py-3 whitespace-nowrap md:text-center">
{% if bag.closed %} {% if bag.closed %}
<span class="pill pill-warning">Closed</span> <span class="pill pill-warning">Closed</span>
{% else %} {% else %}
<span class="pill pill-success">Open</span> <div class="w-14">
<div class="text-xs text-text-muted">{{ bag.remaining }}g left</div> <div class="h-1.5 rounded-full bg-surface-alt overflow-hidden">
<div class="h-full rounded-full bg-accent" style="width: {{ bag.used_percent }}%"></div>
</div>
<div class="mt-0.5 text-text-muted" style="font-variant: small-caps; font-size: 0.7rem">{{ bag.remaining }} / {{ bag.amount }}g</div>
</div>
{% endif %} {% endif %}
</td> </td>
{% if !bag.closed %} {% if !bag.closed %}
<td data-label="Remaining" class="px-4 py-3 whitespace-nowrap md:hidden">{{ bag.remaining }}g left</td> <td data-label="" class="card-progress md:hidden">
<div class="h-1.5 rounded-full bg-surface-alt overflow-hidden">
<div class="h-full rounded-full bg-accent" style="width: {{ bag.used_percent }}%"></div>
</div>
<div class="mt-0.5 text-right text-text-muted" style="font-variant: small-caps; font-size: 0.7rem">{{ bag.remaining }} / {{ bag.amount }}g</div>
</td>
{% endif %} {% endif %}
<td data-label="Finished" class="whitespace-nowrap px-4 py-3 text-xs font-medium text-text-secondary"> <td data-label="Finished" class="mobile-hidden whitespace-nowrap px-4 py-3 text-xs font-medium text-text-secondary">
<div>{{ bag.finished_date }}</div> <div>{{ bag.finished_date }}</div>
</td> </td>
{% if bag.closed %}
<td data-label="Finished" class="whitespace-nowrap px-4 py-3 text-xs font-medium text-text-secondary md:hidden">
<div>{{ bag.finished_date }}</div>
</td>
{% endif %}
{% if is_authenticated %} {% if is_authenticated %}
<td data-label="" class="card-detail hidden"> <td data-label="" class="card-detail hidden">
<div class="flex items-center gap-4"> <div class="flex items-center gap-4">
@ -86,8 +98,6 @@
<span class="md:hidden"> <span class="md:hidden">
{% if bag.closed %} {% if bag.closed %}
<span class="pill pill-warning text-[0.625rem] py-0 px-1.5">Closed</span> <span class="pill pill-warning text-[0.625rem] py-0 px-1.5">Closed</span>
{% else %}
<span class="pill pill-success text-[0.625rem] py-0 px-1.5">Open</span>
{% endif %} {% endif %}
</span> </span>
<button type="button" <button type="button"
@ -102,7 +112,7 @@
</tr> </tr>
{% if is_authenticated %} {% if is_authenticated %}
<tr class="hidden detail-row"> <tr class="hidden detail-row">
<td colspan="7" class="bg-surface-alt px-4 py-2"> <td colspan="6" class="bg-surface-alt px-4 py-2">
<div class="flex items-center gap-4"> <div class="flex items-center gap-4">
{% if !bag.closed %} {% if !bag.closed %}
<button type="button" <button type="button"