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:
parent
3a960b9d14
commit
dcaac1ee9c
5 changed files with 47 additions and 15 deletions
|
|
@ -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"),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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">
|
||||||
|
|
|
||||||
|
|
@ -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"
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue