use std::fmt::Write; use crate::domain::brews::{BrewWithDetails, QuickNote, format_brew_time}; 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, relative_date, }; #[derive(Clone)] pub struct QuickNoteView { pub label: String, pub pill_class: &'static str, pub form_value: String, } impl From for QuickNoteView { fn from(note: QuickNote) -> Self { Self { label: note.label().to_string(), pill_class: if note.is_positive() { "pill pill-success" } else { "pill pill-warning" }, form_value: note.form_value().to_string(), } } } #[derive(Clone)] pub struct BrewView { pub id: String, pub bag_id: i64, pub roast_name: String, pub roaster_name: String, pub roast_slug: String, pub roaster_slug: String, pub coffee_weight: String, pub grinder_id: i64, pub grinder_name: String, pub grinder_model: String, pub grind_setting: String, pub brewer_id: i64, pub brewer_name: String, pub filter_paper_id: Option, pub filter_paper_name: Option, pub water_volume: String, pub water_temp: String, pub ratio: String, pub brew_time: Option, pub quick_notes: Vec, pub quick_notes_label: String, pub quick_notes_raw: String, pub created_date: String, pub created_time: String, pub relative_date_label: String, // Raw values for "brew again" feature pub coffee_weight_raw: f64, pub grind_setting_raw: f64, pub water_volume_raw: i32, pub water_temp_raw: f64, pub brew_time_raw: Option, } impl From for BrewView { fn from(brew: BrewWithDetails) -> Self { let ratio = if brew.brew.coffee_weight > 0.0 { format!( "1:{:.1}", f64::from(brew.brew.water_volume) / brew.brew.coffee_weight ) } else { "\u{2014}".to_string() }; let quick_notes: Vec = brew .brew .quick_notes .iter() .copied() .map(QuickNoteView::from) .collect(); let quick_notes_label = quick_notes .iter() .map(|n| n.label.as_str()) .collect::>() .join(", "); let quick_notes_raw = quick_notes .iter() .map(|n| n.form_value.as_str()) .collect::>() .join(","); let (created_date, created_time) = format_datetime(brew.brew.created_at); Self { id: brew.brew.id.to_string(), bag_id: brew.brew.bag_id.into_inner(), roast_name: brew.roast_name, roaster_name: brew.roaster_name, roast_slug: brew.roast_slug, roaster_slug: brew.roaster_slug, coffee_weight: format_weight(brew.brew.coffee_weight), grinder_id: brew.brew.grinder_id.into_inner(), grinder_model: brew.grinder_model, grinder_name: brew.grinder_name, grind_setting: format!("{:.1}", brew.brew.grind_setting), brewer_id: brew.brew.brewer_id.into_inner(), brewer_name: brew.brewer_name, filter_paper_id: brew .brew .filter_paper_id .map(crate::domain::ids::GearId::into_inner), filter_paper_name: brew.filter_paper_name, water_volume: format!("{}ml", brew.brew.water_volume), water_temp: format!("{:.1}\u{00B0}C", brew.brew.water_temp), ratio, brew_time: brew.brew.brew_time.map(format_brew_time), quick_notes, quick_notes_label, quick_notes_raw, created_date, created_time, relative_date_label: relative_date(brew.brew.created_at), coffee_weight_raw: brew.brew.coffee_weight, grind_setting_raw: brew.brew.grind_setting, water_volume_raw: brew.brew.water_volume, water_temp_raw: brew.brew.water_temp, brew_time_raw: brew.brew.brew_time, } } } impl BrewView { /// Build a URL to the add-brew form pre-filled with this brew's parameters. pub fn brew_again_url(&self) -> String { let mut url = format!( "/add?type=brew&bag_id={}&coffee_weight={}&grinder_id={}&grind_setting={}&brewer_id={}&water_volume={}&water_temp={}", self.bag_id, self.coffee_weight_raw, self.grinder_id, self.grind_setting_raw, self.brewer_id, self.water_volume_raw, self.water_temp_raw, ); if let Some(fp_id) = self.filter_paper_id { let _ = write!(url, "&filter_paper_id={fp_id}"); } if let Some(bt) = self.brew_time_raw { let _ = write!(url, "&brew_time={bt}"); } if !self.quick_notes_raw.is_empty() { let _ = write!(url, "&quick_notes={}", self.quick_notes_raw); } url } } pub struct BrewDefaultsView { pub bag_id: String, pub grinder_id: String, pub grinder_name: String, pub brewer_id: String, pub brewer_name: String, pub filter_paper_id: String, pub filter_paper_name: String, pub coffee_weight: f64, pub grind_setting: f64, pub water_volume: i32, pub water_temp: f64, pub brew_time: Option, /// Comma-separated quick note form values (e.g. "good,too-fast") for pre-filling toggles. pub quick_notes_raw: String, } impl Default for BrewDefaultsView { fn default() -> Self { Self { bag_id: String::new(), grinder_id: String::new(), grinder_name: String::new(), brewer_id: String::new(), brewer_name: String::new(), filter_paper_id: String::new(), filter_paper_name: String::new(), coffee_weight: 15.0, grind_setting: 6.0, water_volume: 250, water_temp: 91.0, brew_time: Some(120), quick_notes_raw: String::new(), } } } pub struct BrewDetailView { 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, // Roaster info pub roaster_country: String, pub roaster_country_flag: String, pub roaster_city: Option, pub roaster_homepage: Option, // Recipe pub coffee_weight: String, pub water_volume: String, pub water_temp: String, pub grind_setting: String, pub brew_time: Option, pub quick_notes_label: String, // Gear pub grinder_name: String, pub brewer_name: String, pub filter_paper_name: Option, // Map pub map_countries: String, pub map_max: u32, pub legend_entries: Vec, // Slugs (for breadcrumbs) pub roaster_slug: String, pub roast_slug: String, // Dates pub created_date: String, pub created_time: String, } impl BrewDetailView { pub fn from_parts(brew: BrewWithDetails, 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(brew.brew.created_at); let quick_notes_label = brew .brew .quick_notes .iter() .map(|n| n.label()) .collect::>() .join(", "); Self { id: brew.brew.id.to_string(), roast_name: brew.roast_name, roaster_name: brew.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, coffee_weight: format_weight(brew.brew.coffee_weight), water_volume: format!("{}ml", brew.brew.water_volume), water_temp: format!("{:.1}\u{00B0}C", brew.brew.water_temp), grind_setting: format!("{:.1}", brew.brew.grind_setting), brew_time: brew.brew.brew_time.map(format_brew_time), quick_notes_label, grinder_name: brew.grinder_name, brewer_name: brew.brewer_name, filter_paper_name: brew.filter_paper_name, map_countries, map_max, legend_entries, created_date, created_time, } } } impl From for BrewDefaultsView { fn from(brew: BrewWithDetails) -> Self { Self { bag_id: brew.brew.bag_id.to_string(), grinder_id: brew.brew.grinder_id.to_string(), grinder_name: brew.grinder_name, brewer_id: brew.brew.brewer_id.to_string(), brewer_name: brew.brewer_name, filter_paper_id: brew .brew .filter_paper_id .map(|id| id.to_string()) .unwrap_or_default(), filter_paper_name: brew.filter_paper_name.unwrap_or_default(), coffee_weight: brew.brew.coffee_weight, grind_setting: brew.brew.grind_setting, water_volume: brew.brew.water_volume, water_temp: brew.brew.water_temp, brew_time: brew.brew.brew_time, quick_notes_raw: String::new(), } } }