brewlog/src/presentation/web/views/bags.rs
Jon Seager 766ac1432d
refactor: extract duplicated helpers for tasting notes, used_percent, format_datetime, and map builder
- Add parse_and_categorize() in tasting_notes.rs, replacing identical
  split-trim-categorize pipelines in mod.rs, roasts.rs, and timeline.rs
- Add used_percent() in bags.rs, deduplicating the calculation in
  BagView::from_domain and BagDetailView::from_parts
- Add format_datetime() in views/mod.rs, replacing 16 paired occurrences
  of .format("%Y-%m-%d") / .format("%H:%M") across 7 view files
- Add build_origin_roaster_map() in views/mod.rs, replacing identical
  map-entry building blocks in bags.rs, brews.rs, and roasts.rs detail views
2026-02-13 16:20:21 +00:00

164 lines
5 KiB
Rust

use crate::domain::bags::BagWithRoast;
use crate::domain::formatting::format_weight;
use crate::domain::roasters::Roaster;
use crate::domain::roasts::Roast;
use super::tasting_notes::TastingNoteView;
use super::{
LegendEntry, build_coffee_info, build_origin_roaster_map, build_roaster_info, format_datetime,
};
fn used_percent(amount: f64, remaining: f64) -> u8 {
if amount > 0.0 {
(((amount - remaining) / amount) * 100.0).clamp(0.0, 100.0) as u8
} else {
0
}
}
#[derive(Debug, Clone)]
pub struct BagView {
pub id: String,
pub roast_id: String,
pub roast_date: Option<String>,
pub amount: String,
pub remaining: String,
pub closed: bool,
pub finished_date: String,
pub created_date: String,
pub created_time: String,
pub roast_name: String,
pub roaster_name: String,
pub roast_slug: String,
pub roaster_slug: String,
pub used_percent: u8,
}
impl BagView {
pub fn from_domain(bag: BagWithRoast) -> Self {
let used_percent = used_percent(bag.bag.amount, bag.bag.remaining);
let (created_date, created_time) = format_datetime(bag.bag.created_at);
Self {
id: bag.bag.id.to_string(),
roast_id: bag.bag.roast_id.to_string(),
roast_date: bag.bag.roast_date.map(|d| d.to_string()),
amount: format_weight(bag.bag.amount),
remaining: format_weight(bag.bag.remaining),
closed: bag.bag.closed,
finished_date: bag
.bag
.finished_at
.map_or_else(|| "".to_string(), |d| d.format("%Y-%m-%d").to_string()),
created_date,
created_time,
roast_name: bag.roast_name,
roaster_name: bag.roaster_name,
roast_slug: bag.roast_slug,
roaster_slug: bag.roaster_slug,
used_percent,
}
}
}
#[derive(Clone)]
pub struct BagOptionView {
pub id: String,
pub label: String,
pub roast_name: String,
pub roaster_name: String,
pub remaining: String,
}
pub struct BagDetailView {
pub id: String,
// Coffee info
pub roast_name: String,
pub roaster_name: String,
pub origin: String,
pub origin_flag: String,
pub region: String,
pub producer: String,
pub process: String,
pub tasting_notes: Vec<TastingNoteView>,
// Roaster info
pub roaster_country: String,
pub roaster_country_flag: String,
pub roaster_city: Option<String>,
pub roaster_homepage: Option<String>,
// Bag-specific
pub amount: String,
pub remaining: String,
pub used_percent: u8,
pub closed: bool,
pub roast_date: Option<String>,
pub finished_date: Option<String>,
// Map
pub map_countries: String,
pub map_max: u32,
pub legend_entries: Vec<LegendEntry>,
// Slugs (for breadcrumbs)
pub roaster_slug: String,
pub roast_slug: String,
// Dates
pub created_date: String,
pub created_time: String,
}
impl BagDetailView {
pub fn from_parts(bag: BagWithRoast, roast: &Roast, roaster: &Roaster) -> Self {
let coffee = build_coffee_info(roast);
let roaster_info = build_roaster_info(roaster);
let (map_countries, map_max, legend_entries) =
build_origin_roaster_map(roast.origin.as_deref(), &roaster.country);
let (created_date, created_time) = format_datetime(bag.bag.created_at);
Self {
id: bag.bag.id.to_string(),
roast_name: bag.roast_name,
roaster_name: bag.roaster_name,
roaster_slug: roaster.slug.clone(),
roast_slug: roast.slug.clone(),
origin: coffee.origin,
origin_flag: coffee.origin_flag,
region: coffee.region,
producer: coffee.producer,
process: coffee.process,
tasting_notes: coffee.tasting_notes,
roaster_country: roaster_info.country,
roaster_country_flag: roaster_info.country_flag,
roaster_city: roaster_info.city,
roaster_homepage: roaster_info.homepage,
amount: format_weight(bag.bag.amount),
remaining: format_weight(bag.bag.remaining),
used_percent: used_percent(bag.bag.amount, bag.bag.remaining),
closed: bag.bag.closed,
roast_date: bag.bag.roast_date.map(|d| d.to_string()),
finished_date: bag
.bag
.finished_at
.map(|d| d.format("%Y-%m-%d").to_string()),
map_countries,
map_max,
legend_entries,
created_date,
created_time,
}
}
}
impl From<BagWithRoast> for BagOptionView {
fn from(bag: BagWithRoast) -> Self {
let remaining = format_weight(bag.bag.remaining);
Self {
id: bag.bag.id.to_string(),
label: format!(
"{} - {} ({} remaining)",
bag.roaster_name, bag.roast_name, remaining
),
roast_name: bag.roast_name,
roaster_name: bag.roaster_name,
remaining,
}
}
}