brewlog/src/domain/bags.rs
Jon Seager 3a960b9d14
feat(ui): enhance desktop tables and mobile card views
- Add spacious flat table CSS for desktop (transparent card wrapper,
  lighter dividers, visible hover, larger cell padding)
- Split roaster/cafe columns into separate Country and City columns
  with sortable headers
- Split cup Roast/Roaster into separate desktop columns with city
  sort support (full-stack: domain, repo, view, template)
- Add sortable Status column to bags with colored pills (green Open,
  amber Closed) and remaining weight subtext
- Style bag Finished column as date-only (NaiveDate) and Weight
  column with smaller text
- Show status pill next to mobile bag card actions menu
- Add city to cups mobile card view
- Use middle dot separator for brew recipe weight/volume
- Add tasting notes pills to roast list, brew notes to brew list
- Fix build.rs to use rerun-if-changed for CSS and templates
2026-02-06 15:40:40 +00:00

161 lines
4.1 KiB
Rust

use chrono::{DateTime, NaiveDate, Utc};
use serde::{Deserialize, Serialize};
use super::ids::{BagId, RoastId};
use super::listing::{SortDirection, SortKey};
use crate::domain::roasters::Roaster;
use crate::domain::roasts::Roast;
use crate::domain::timeline::{NewTimelineEvent, TimelineEventDetail};
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct Bag {
pub id: BagId,
pub roast_id: RoastId,
pub roast_date: Option<NaiveDate>,
pub amount: f64,
pub remaining: f64,
pub closed: bool,
pub finished_at: Option<NaiveDate>,
pub created_at: DateTime<Utc>,
pub updated_at: DateTime<Utc>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct BagWithRoast {
#[serde(flatten)]
pub bag: Bag,
pub roast_name: String,
pub roaster_name: String,
pub roast_slug: String,
pub roaster_slug: String,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct NewBag {
pub roast_id: RoastId,
pub roast_date: Option<NaiveDate>,
pub amount: f64,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct UpdateBag {
pub remaining: Option<f64>,
pub closed: Option<bool>,
pub finished_at: Option<NaiveDate>,
}
/// Filter criteria for bag queries.
#[derive(Debug, Default, Clone)]
pub struct BagFilter {
pub closed: Option<bool>,
pub roast_id: Option<RoastId>,
}
impl BagFilter {
/// No filter - returns all bags.
pub fn all() -> Self {
Self::default()
}
/// Filter for open (unclosed) bags only.
pub fn open() -> Self {
Self {
closed: Some(false),
..Default::default()
}
}
/// Filter for closed bags only.
pub fn closed() -> Self {
Self {
closed: Some(true),
..Default::default()
}
}
/// Filter for bags of a specific roast.
pub fn for_roast(roast_id: RoastId) -> Self {
Self {
roast_id: Some(roast_id),
..Default::default()
}
}
}
#[derive(Debug, Clone, Copy, Eq, PartialEq)]
pub enum BagSortKey {
RoastDate,
CreatedAt,
UpdatedAt,
Roaster,
Roast,
Status,
FinishedAt,
}
impl SortKey for BagSortKey {
fn default() -> Self {
BagSortKey::CreatedAt
}
fn from_query(value: &str) -> Option<Self> {
match value {
"roast-date" => Some(BagSortKey::RoastDate),
"created-at" => Some(BagSortKey::CreatedAt),
"updated-at" => Some(BagSortKey::UpdatedAt),
"roaster" => Some(BagSortKey::Roaster),
"roast" => Some(BagSortKey::Roast),
"status" => Some(BagSortKey::Status),
"finished-at" => Some(BagSortKey::FinishedAt),
_ => None,
}
}
fn query_value(self) -> &'static str {
match self {
BagSortKey::RoastDate => "roast-date",
BagSortKey::CreatedAt => "created-at",
BagSortKey::UpdatedAt => "updated-at",
BagSortKey::Roaster => "roaster",
BagSortKey::Roast => "roast",
BagSortKey::Status => "status",
BagSortKey::FinishedAt => "finished-at",
}
}
fn default_direction(self) -> SortDirection {
match self {
BagSortKey::Roaster | BagSortKey::Roast | BagSortKey::Status => SortDirection::Asc,
_ => SortDirection::Desc,
}
}
}
pub fn bag_timeline_event(
bag: &Bag,
action: &str,
roast: &Roast,
roaster: &Roaster,
) -> NewTimelineEvent {
NewTimelineEvent {
entity_type: "bag".to_string(),
entity_id: bag.id.into_inner(),
action: action.to_string(),
occurred_at: Utc::now(),
title: roast.name.clone(),
details: vec![
TimelineEventDetail {
label: "Roaster".to_string(),
value: roaster.name.clone(),
},
TimelineEventDetail {
label: "Amount".to_string(),
value: format!("{}g", bag.amount),
},
],
tasting_notes: vec![],
slug: Some(roast.slug.clone()),
roaster_slug: Some(roaster.slug.clone()),
brew_data: None,
}
}