- 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
164 lines
5 KiB
Rust
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,
|
|
}
|
|
}
|
|
}
|