feat(brews): add quick notes
Predefined toggleable tags for brews: Good, Too Fast, Too Slow, Too Hot, Under Extracted, Over Extracted. - Add quick_notes TEXT column storing JSON array - Add QuickNote enum with label/form-value/positive helpers - Update repository to encode/decode JSON, backup to export notes - Add toggle pill UI on brew form with Datastar signals - Show pills on home page cards (muted for Good, amber for others) - Show comma-joined text in data table Notes column - Add --quick-notes flag to CLI, update HTTP client - Update bootstrap script with sample quick notes
This commit is contained in:
parent
3bc6fae656
commit
010b700595
18 changed files with 286 additions and 17 deletions
4
build.rs
4
build.rs
|
|
@ -25,6 +25,10 @@ fn tailwind() {
|
||||||
println!("cargo:rerun-if-changed={entry}");
|
println!("cargo:rerun-if-changed={entry}");
|
||||||
}
|
}
|
||||||
for entry in walkdir("static") {
|
for entry in walkdir("static") {
|
||||||
|
// Skip the generated output file to avoid circular rebuilds
|
||||||
|
if entry == "static/css/styles.css" {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
println!("cargo:rerun-if-changed={entry}");
|
println!("cargo:rerun-if-changed={entry}");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
1
migrations/0021_add_brew_notes.sql
Normal file
1
migrations/0021_add_brew_notes.sql
Normal file
|
|
@ -0,0 +1 @@
|
||||||
|
ALTER TABLE brews ADD COLUMN quick_notes TEXT;
|
||||||
|
|
@ -754,7 +754,8 @@ fi
|
||||||
--brewer-id "$(./target/debug/brewlog gear list | jq -r '.[] | select(.model=="V60 02") | .id')" \
|
--brewer-id "$(./target/debug/brewlog gear list | jq -r '.[] | select(.model=="V60 02") | .id')" \
|
||||||
--filter-paper-id "$(./target/debug/brewlog gear list | jq -r '.[] | select(.model=="V60 Tabbed 02") | .id')" \
|
--filter-paper-id "$(./target/debug/brewlog gear list | jq -r '.[] | select(.model=="V60 Tabbed 02") | .id')" \
|
||||||
--water-volume 250 \
|
--water-volume 250 \
|
||||||
--water-temp 92.0
|
--water-temp 92.0 \
|
||||||
|
--quick-notes good
|
||||||
|
|
||||||
# AeroPress brew - Finca Tamana with J-Max (ratio 1:15)
|
# AeroPress brew - Finca Tamana with J-Max (ratio 1:15)
|
||||||
./target/debug/brewlog brew add \
|
./target/debug/brewlog brew add \
|
||||||
|
|
@ -786,7 +787,8 @@ fi
|
||||||
--brewer-id "$(./target/debug/brewlog gear list | jq -r '.[] | select(.model=="Stagg XF") | .id')" \
|
--brewer-id "$(./target/debug/brewlog gear list | jq -r '.[] | select(.model=="Stagg XF") | .id')" \
|
||||||
--filter-paper-id "$(./target/debug/brewlog gear list | jq -r '.[] | select(.model=="Stagg XF Filters") | .id')" \
|
--filter-paper-id "$(./target/debug/brewlog gear list | jq -r '.[] | select(.model=="Stagg XF Filters") | .id')" \
|
||||||
--water-volume 300 \
|
--water-volume 300 \
|
||||||
--water-temp 93.0
|
--water-temp 93.0 \
|
||||||
|
--quick-notes too-fast,under-extracted
|
||||||
|
|
||||||
# Light V60 brew - Red Brick Espresso with Comandante (ratio 1:16.7)
|
# Light V60 brew - Red Brick Espresso with Comandante (ratio 1:16.7)
|
||||||
./target/debug/brewlog brew add \
|
./target/debug/brewlog brew add \
|
||||||
|
|
@ -807,7 +809,8 @@ fi
|
||||||
--grind-setting 18.0 \
|
--grind-setting 18.0 \
|
||||||
--brewer-id "$(./target/debug/brewlog gear list | jq -r '.[] | select(.model=="Original") | .id')" \
|
--brewer-id "$(./target/debug/brewlog gear list | jq -r '.[] | select(.model=="Original") | .id')" \
|
||||||
--water-volume 225 \
|
--water-volume 225 \
|
||||||
--water-temp 85.0
|
--water-temp 85.0 \
|
||||||
|
--quick-notes too-hot,over-extracted
|
||||||
|
|
||||||
# V60 brew - Suke Quto with J-Max (ratio 1:16)
|
# V60 brew - Suke Quto with J-Max (ratio 1:16)
|
||||||
./target/debug/brewlog brew add \
|
./target/debug/brewlog brew add \
|
||||||
|
|
@ -818,7 +821,8 @@ fi
|
||||||
--brewer-id "$(./target/debug/brewlog gear list | jq -r '.[] | select(.model=="V60 02") | .id')" \
|
--brewer-id "$(./target/debug/brewlog gear list | jq -r '.[] | select(.model=="V60 02") | .id')" \
|
||||||
--filter-paper-id "$(./target/debug/brewlog gear list | jq -r '.[] | select(.model=="FAST Specialty 02") | .id')" \
|
--filter-paper-id "$(./target/debug/brewlog gear list | jq -r '.[] | select(.model=="FAST Specialty 02") | .id')" \
|
||||||
--water-volume 320 \
|
--water-volume 320 \
|
||||||
--water-temp 92.0
|
--water-temp 92.0 \
|
||||||
|
--quick-notes good
|
||||||
|
|
||||||
# Stagg XF brew - Simbi with Comandante (ratio 1:16)
|
# Stagg XF brew - Simbi with Comandante (ratio 1:16)
|
||||||
./target/debug/brewlog brew add \
|
./target/debug/brewlog brew add \
|
||||||
|
|
|
||||||
|
|
@ -13,7 +13,7 @@ use crate::application::routes::support::{
|
||||||
};
|
};
|
||||||
use crate::application::server::AppState;
|
use crate::application::server::AppState;
|
||||||
use crate::domain::bags::BagFilter;
|
use crate::domain::bags::BagFilter;
|
||||||
use crate::domain::brews::{BrewFilter, BrewSortKey, BrewWithDetails, NewBrew};
|
use crate::domain::brews::{BrewFilter, BrewSortKey, BrewWithDetails, NewBrew, QuickNote};
|
||||||
use crate::domain::gear::{GearCategory, GearFilter, GearSortKey};
|
use crate::domain::gear::{GearCategory, GearFilter, GearSortKey};
|
||||||
use crate::domain::ids::{BagId, BrewId, GearId};
|
use crate::domain::ids::{BagId, BrewId, GearId};
|
||||||
use crate::domain::listing::{ListRequest, PageSize, SortDirection};
|
use crate::domain::listing::{ListRequest, PageSize, SortDirection};
|
||||||
|
|
@ -21,6 +21,7 @@ use crate::domain::timeline::{NewTimelineEvent, TimelineBrewData, TimelineEventD
|
||||||
use crate::presentation::web::templates::BrewListTemplate;
|
use crate::presentation::web::templates::BrewListTemplate;
|
||||||
use crate::presentation::web::views::{
|
use crate::presentation::web::views::{
|
||||||
BagOptionView, BrewDefaultsView, BrewView, GearOptionView, ListNavigator, Paginated,
|
BagOptionView, BrewDefaultsView, BrewView, GearOptionView, ListNavigator, Paginated,
|
||||||
|
QuickNoteView,
|
||||||
};
|
};
|
||||||
|
|
||||||
const BREW_PAGE_PATH: &str = "/data?type=brews";
|
const BREW_PAGE_PATH: &str = "/data?type=brews";
|
||||||
|
|
@ -37,6 +38,7 @@ pub(crate) struct BrewFormData {
|
||||||
pub(crate) brewer_options: Vec<GearOptionView>,
|
pub(crate) brewer_options: Vec<GearOptionView>,
|
||||||
pub(crate) filter_paper_options: Vec<GearOptionView>,
|
pub(crate) filter_paper_options: Vec<GearOptionView>,
|
||||||
pub(crate) defaults: BrewDefaultsView,
|
pub(crate) defaults: BrewDefaultsView,
|
||||||
|
pub(crate) quick_note_options: Vec<QuickNoteView>,
|
||||||
}
|
}
|
||||||
|
|
||||||
pub(crate) async fn load_brew_form_data(state: &AppState) -> Result<BrewFormData, AppError> {
|
pub(crate) async fn load_brew_form_data(state: &AppState) -> Result<BrewFormData, AppError> {
|
||||||
|
|
@ -80,12 +82,19 @@ pub(crate) async fn load_brew_form_data(state: &AppState) -> Result<BrewFormData
|
||||||
.map(|b| BrewDefaultsView::from(b.brew))
|
.map(|b| BrewDefaultsView::from(b.brew))
|
||||||
.unwrap_or_default();
|
.unwrap_or_default();
|
||||||
|
|
||||||
|
let quick_note_options = QuickNote::all()
|
||||||
|
.iter()
|
||||||
|
.copied()
|
||||||
|
.map(QuickNoteView::from)
|
||||||
|
.collect();
|
||||||
|
|
||||||
Ok(BrewFormData {
|
Ok(BrewFormData {
|
||||||
bag_options,
|
bag_options,
|
||||||
grinder_options,
|
grinder_options,
|
||||||
brewer_options,
|
brewer_options,
|
||||||
filter_paper_options,
|
filter_paper_options,
|
||||||
defaults,
|
defaults,
|
||||||
|
quick_note_options,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -147,6 +156,26 @@ where
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn deserialize_quick_notes<'de, D>(deserializer: D) -> Result<Vec<QuickNote>, D::Error>
|
||||||
|
where
|
||||||
|
D: Deserializer<'de>,
|
||||||
|
{
|
||||||
|
let value: Option<serde_json::Value> = Option::deserialize(deserializer)?;
|
||||||
|
match value {
|
||||||
|
None | Some(serde_json::Value::Null) => Ok(Vec::new()),
|
||||||
|
Some(serde_json::Value::String(s)) if s.is_empty() => Ok(Vec::new()),
|
||||||
|
Some(serde_json::Value::String(s)) => Ok(s
|
||||||
|
.split(',')
|
||||||
|
.filter_map(|v| QuickNote::from_str_value(v.trim()))
|
||||||
|
.collect()),
|
||||||
|
Some(serde_json::Value::Array(arr)) => Ok(arr
|
||||||
|
.iter()
|
||||||
|
.filter_map(|v| v.as_str().and_then(QuickNote::from_str_value))
|
||||||
|
.collect()),
|
||||||
|
Some(_) => Err(serde::de::Error::custom("invalid quick_notes")),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
#[derive(Debug, Deserialize)]
|
#[derive(Debug, Deserialize)]
|
||||||
pub(crate) struct NewBrewSubmission {
|
pub(crate) struct NewBrewSubmission {
|
||||||
bag_id: BagId,
|
bag_id: BagId,
|
||||||
|
|
@ -158,6 +187,8 @@ pub(crate) struct NewBrewSubmission {
|
||||||
filter_paper_id: Option<GearId>,
|
filter_paper_id: Option<GearId>,
|
||||||
water_volume: i32,
|
water_volume: i32,
|
||||||
water_temp: f64,
|
water_temp: f64,
|
||||||
|
#[serde(default, deserialize_with = "deserialize_quick_notes")]
|
||||||
|
quick_notes: Vec<QuickNote>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl NewBrewSubmission {
|
impl NewBrewSubmission {
|
||||||
|
|
@ -186,6 +217,7 @@ impl NewBrewSubmission {
|
||||||
filter_paper_id: self.filter_paper_id,
|
filter_paper_id: self.filter_paper_id,
|
||||||
water_volume: self.water_volume,
|
water_volume: self.water_volume,
|
||||||
water_temp: self.water_temp,
|
water_temp: self.water_temp,
|
||||||
|
quick_notes: self.quick_notes,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -309,6 +341,19 @@ fn brew_timeline_event(enriched: &BrewWithDetails) -> NewTimelineEvent {
|
||||||
value: ratio,
|
value: ratio,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
if !enriched.brew.quick_notes.is_empty() {
|
||||||
|
let labels: Vec<&str> = enriched
|
||||||
|
.brew
|
||||||
|
.quick_notes
|
||||||
|
.iter()
|
||||||
|
.map(|n| n.label())
|
||||||
|
.collect();
|
||||||
|
details.push(TimelineEventDetail {
|
||||||
|
label: "Notes".to_string(),
|
||||||
|
value: labels.join(", "),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
NewTimelineEvent {
|
NewTimelineEvent {
|
||||||
entity_type: "brew".to_string(),
|
entity_type: "brew".to_string(),
|
||||||
entity_id: enriched.brew.id.into_inner(),
|
entity_id: enriched.brew.id.into_inner(),
|
||||||
|
|
|
||||||
|
|
@ -99,6 +99,7 @@ pub(crate) async fn add_page(
|
||||||
filter_paper_options: brew_form.filter_paper_options,
|
filter_paper_options: brew_form.filter_paper_options,
|
||||||
cafe_options,
|
cafe_options,
|
||||||
defaults: brew_form.defaults,
|
defaults: brew_form.defaults,
|
||||||
|
quick_note_options: brew_form.quick_note_options,
|
||||||
};
|
};
|
||||||
|
|
||||||
render_html(template).map(IntoResponse::into_response)
|
render_html(template).map(IntoResponse::into_response)
|
||||||
|
|
|
||||||
|
|
@ -4,6 +4,67 @@ use serde::{Deserialize, Serialize};
|
||||||
use super::ids::{BagId, BrewId, GearId};
|
use super::ids::{BagId, BrewId, GearId};
|
||||||
use super::listing::{SortDirection, SortKey};
|
use super::listing::{SortDirection, SortKey};
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
|
||||||
|
pub enum QuickNote {
|
||||||
|
Good,
|
||||||
|
TooFast,
|
||||||
|
TooSlow,
|
||||||
|
TooHot,
|
||||||
|
UnderExtracted,
|
||||||
|
OverExtracted,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl QuickNote {
|
||||||
|
pub fn label(self) -> &'static str {
|
||||||
|
match self {
|
||||||
|
Self::Good => "Good",
|
||||||
|
Self::TooFast => "Too Fast",
|
||||||
|
Self::TooSlow => "Too Slow",
|
||||||
|
Self::TooHot => "Too Hot",
|
||||||
|
Self::UnderExtracted => "Under Extracted",
|
||||||
|
Self::OverExtracted => "Over Extracted",
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn form_value(self) -> &'static str {
|
||||||
|
match self {
|
||||||
|
Self::Good => "good",
|
||||||
|
Self::TooFast => "too-fast",
|
||||||
|
Self::TooSlow => "too-slow",
|
||||||
|
Self::TooHot => "too-hot",
|
||||||
|
Self::UnderExtracted => "under-extracted",
|
||||||
|
Self::OverExtracted => "over-extracted",
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn from_str_value(s: &str) -> Option<Self> {
|
||||||
|
match s {
|
||||||
|
"good" | "Good" => Some(Self::Good),
|
||||||
|
"too-fast" | "Too Fast" => Some(Self::TooFast),
|
||||||
|
"too-slow" | "Too Slow" => Some(Self::TooSlow),
|
||||||
|
"too-hot" | "Too Hot" => Some(Self::TooHot),
|
||||||
|
"under-extracted" | "Under Extracted" => Some(Self::UnderExtracted),
|
||||||
|
"over-extracted" | "Over Extracted" => Some(Self::OverExtracted),
|
||||||
|
_ => None,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn is_positive(self) -> bool {
|
||||||
|
matches!(self, Self::Good)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn all() -> &'static [Self] {
|
||||||
|
&[
|
||||||
|
Self::Good,
|
||||||
|
Self::TooFast,
|
||||||
|
Self::TooSlow,
|
||||||
|
Self::TooHot,
|
||||||
|
Self::UnderExtracted,
|
||||||
|
Self::OverExtracted,
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||||
pub struct Brew {
|
pub struct Brew {
|
||||||
pub id: BrewId,
|
pub id: BrewId,
|
||||||
|
|
@ -15,6 +76,7 @@ pub struct Brew {
|
||||||
pub filter_paper_id: Option<GearId>,
|
pub filter_paper_id: Option<GearId>,
|
||||||
pub water_volume: i32,
|
pub water_volume: i32,
|
||||||
pub water_temp: f64,
|
pub water_temp: f64,
|
||||||
|
pub quick_notes: Vec<QuickNote>,
|
||||||
pub created_at: DateTime<Utc>,
|
pub created_at: DateTime<Utc>,
|
||||||
pub updated_at: DateTime<Utc>,
|
pub updated_at: DateTime<Utc>,
|
||||||
}
|
}
|
||||||
|
|
@ -42,6 +104,7 @@ pub struct NewBrew {
|
||||||
pub filter_paper_id: Option<GearId>,
|
pub filter_paper_id: Option<GearId>,
|
||||||
pub water_volume: i32,
|
pub water_volume: i32,
|
||||||
pub water_temp: f64,
|
pub water_temp: f64,
|
||||||
|
pub quick_notes: Vec<QuickNote>,
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Filter criteria for brew queries.
|
/// Filter criteria for brew queries.
|
||||||
|
|
|
||||||
|
|
@ -6,7 +6,7 @@ use serde::{Deserialize, Serialize};
|
||||||
use serde_json::{from_str, to_string};
|
use serde_json::{from_str, to_string};
|
||||||
|
|
||||||
use crate::domain::bags::Bag;
|
use crate::domain::bags::Bag;
|
||||||
use crate::domain::brews::Brew;
|
use crate::domain::brews::{Brew, QuickNote};
|
||||||
use crate::domain::cafes::Cafe;
|
use crate::domain::cafes::Cafe;
|
||||||
use crate::domain::gear::{Gear, GearCategory};
|
use crate::domain::gear::{Gear, GearCategory};
|
||||||
use crate::domain::ids::{BagId, BrewId, CafeId, GearId, RoastId, RoasterId, TimelineEventId};
|
use crate::domain::ids::{BagId, BrewId, CafeId, GearId, RoastId, RoasterId, TimelineEventId};
|
||||||
|
|
@ -164,7 +164,7 @@ impl BackupService {
|
||||||
|
|
||||||
async fn export_brews(&self) -> anyhow::Result<Vec<Brew>> {
|
async fn export_brews(&self) -> anyhow::Result<Vec<Brew>> {
|
||||||
let records = sqlx::query_as::<_, BrewRecord>(
|
let records = sqlx::query_as::<_, BrewRecord>(
|
||||||
"SELECT id, bag_id, coffee_weight, grinder_id, grind_setting, brewer_id, filter_paper_id, water_volume, water_temp, created_at, updated_at FROM brews ORDER BY id",
|
"SELECT id, bag_id, coffee_weight, grinder_id, grind_setting, brewer_id, filter_paper_id, water_volume, water_temp, quick_notes, created_at, updated_at FROM brews ORDER BY id",
|
||||||
)
|
)
|
||||||
.fetch_all(&self.pool)
|
.fetch_all(&self.pool)
|
||||||
.await
|
.await
|
||||||
|
|
@ -561,12 +561,21 @@ struct BrewRecord {
|
||||||
filter_paper_id: Option<i64>,
|
filter_paper_id: Option<i64>,
|
||||||
water_volume: i32,
|
water_volume: i32,
|
||||||
water_temp: f64,
|
water_temp: f64,
|
||||||
|
quick_notes: Option<String>,
|
||||||
created_at: DateTime<Utc>,
|
created_at: DateTime<Utc>,
|
||||||
updated_at: DateTime<Utc>,
|
updated_at: DateTime<Utc>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl BrewRecord {
|
impl BrewRecord {
|
||||||
fn into_domain(self) -> Brew {
|
fn into_domain(self) -> Brew {
|
||||||
|
let quick_notes = match self.quick_notes {
|
||||||
|
Some(s) if !s.is_empty() => serde_json::from_str::<Vec<String>>(&s)
|
||||||
|
.unwrap_or_default()
|
||||||
|
.iter()
|
||||||
|
.filter_map(|v| QuickNote::from_str_value(v))
|
||||||
|
.collect(),
|
||||||
|
_ => Vec::new(),
|
||||||
|
};
|
||||||
Brew {
|
Brew {
|
||||||
id: BrewId::new(self.id),
|
id: BrewId::new(self.id),
|
||||||
bag_id: BagId::new(self.bag_id),
|
bag_id: BagId::new(self.bag_id),
|
||||||
|
|
@ -577,6 +586,7 @@ impl BrewRecord {
|
||||||
filter_paper_id: self.filter_paper_id.map(GearId::new),
|
filter_paper_id: self.filter_paper_id.map(GearId::new),
|
||||||
water_volume: self.water_volume,
|
water_volume: self.water_volume,
|
||||||
water_temp: self.water_temp,
|
water_temp: self.water_temp,
|
||||||
|
quick_notes,
|
||||||
created_at: self.created_at,
|
created_at: self.created_at,
|
||||||
updated_at: self.updated_at,
|
updated_at: self.updated_at,
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,6 @@
|
||||||
use anyhow::{Context, Result};
|
use anyhow::{Context, Result};
|
||||||
|
|
||||||
use crate::domain::brews::BrewWithDetails;
|
use crate::domain::brews::{BrewWithDetails, QuickNote};
|
||||||
use crate::domain::ids::{BagId, BrewId, GearId};
|
use crate::domain::ids::{BagId, BrewId, GearId};
|
||||||
|
|
||||||
use super::BrewlogClient;
|
use super::BrewlogClient;
|
||||||
|
|
@ -25,6 +25,7 @@ impl<'a> BrewsClient<'a> {
|
||||||
filter_paper_id: Option<GearId>,
|
filter_paper_id: Option<GearId>,
|
||||||
water_volume: i32,
|
water_volume: i32,
|
||||||
water_temp: f64,
|
water_temp: f64,
|
||||||
|
quick_notes: Vec<QuickNote>,
|
||||||
) -> Result<BrewWithDetails> {
|
) -> Result<BrewWithDetails> {
|
||||||
let url = self.inner.endpoint("api/v1/brews")?;
|
let url = self.inner.endpoint("api/v1/brews")?;
|
||||||
let mut payload = serde_json::json!({
|
let mut payload = serde_json::json!({
|
||||||
|
|
@ -39,6 +40,10 @@ impl<'a> BrewsClient<'a> {
|
||||||
if let Some(fp_id) = filter_paper_id {
|
if let Some(fp_id) = filter_paper_id {
|
||||||
payload["filter_paper_id"] = serde_json::json!(fp_id);
|
payload["filter_paper_id"] = serde_json::json!(fp_id);
|
||||||
}
|
}
|
||||||
|
if !quick_notes.is_empty() {
|
||||||
|
let labels: Vec<&str> = quick_notes.iter().map(|n| n.label()).collect();
|
||||||
|
payload["quick_notes"] = serde_json::json!(labels);
|
||||||
|
}
|
||||||
|
|
||||||
let response = self
|
let response = self
|
||||||
.inner
|
.inner
|
||||||
|
|
|
||||||
|
|
@ -3,7 +3,7 @@ use chrono::{DateTime, Utc};
|
||||||
use sqlx::query_as;
|
use sqlx::query_as;
|
||||||
|
|
||||||
use crate::domain::RepositoryError;
|
use crate::domain::RepositoryError;
|
||||||
use crate::domain::brews::{Brew, BrewFilter, BrewSortKey, BrewWithDetails, NewBrew};
|
use crate::domain::brews::{Brew, BrewFilter, BrewSortKey, BrewWithDetails, NewBrew, QuickNote};
|
||||||
use crate::domain::ids::{BagId, BrewId, GearId};
|
use crate::domain::ids::{BagId, BrewId, GearId};
|
||||||
use crate::domain::listing::{ListRequest, Page, SortDirection};
|
use crate::domain::listing::{ListRequest, Page, SortDirection};
|
||||||
use crate::domain::repositories::BrewRepository;
|
use crate::domain::repositories::BrewRepository;
|
||||||
|
|
@ -13,6 +13,7 @@ const BASE_SELECT: &str = r"
|
||||||
SELECT
|
SELECT
|
||||||
br.id, br.bag_id, br.coffee_weight, br.grinder_id, br.grind_setting,
|
br.id, br.bag_id, br.coffee_weight, br.grinder_id, br.grind_setting,
|
||||||
br.brewer_id, br.filter_paper_id, br.water_volume, br.water_temp,
|
br.brewer_id, br.filter_paper_id, br.water_volume, br.water_temp,
|
||||||
|
br.quick_notes,
|
||||||
br.created_at, br.updated_at,
|
br.created_at, br.updated_at,
|
||||||
r.name as roast_name, r.slug as roast_slug,
|
r.name as roast_name, r.slug as roast_slug,
|
||||||
rr.name as roaster_name, rr.slug as roaster_slug,
|
rr.name as roaster_name, rr.slug as roaster_slug,
|
||||||
|
|
@ -51,6 +52,26 @@ impl SqlBrewRepository {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn decode_quick_notes(raw: Option<String>) -> Vec<QuickNote> {
|
||||||
|
match raw {
|
||||||
|
Some(s) if !s.is_empty() => serde_json::from_str::<Vec<String>>(&s)
|
||||||
|
.unwrap_or_default()
|
||||||
|
.iter()
|
||||||
|
.filter_map(|v| QuickNote::from_str_value(v))
|
||||||
|
.collect(),
|
||||||
|
_ => Vec::new(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn encode_quick_notes(notes: &[QuickNote]) -> Option<String> {
|
||||||
|
if notes.is_empty() {
|
||||||
|
None
|
||||||
|
} else {
|
||||||
|
let labels: Vec<&str> = notes.iter().map(|n| n.label()).collect();
|
||||||
|
serde_json::to_string(&labels).ok()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
fn to_domain(record: BrewRecord) -> Brew {
|
fn to_domain(record: BrewRecord) -> Brew {
|
||||||
Brew {
|
Brew {
|
||||||
id: BrewId::new(record.id),
|
id: BrewId::new(record.id),
|
||||||
|
|
@ -62,6 +83,7 @@ impl SqlBrewRepository {
|
||||||
filter_paper_id: record.filter_paper_id.map(GearId::new),
|
filter_paper_id: record.filter_paper_id.map(GearId::new),
|
||||||
water_volume: record.water_volume,
|
water_volume: record.water_volume,
|
||||||
water_temp: record.water_temp,
|
water_temp: record.water_temp,
|
||||||
|
quick_notes: Self::decode_quick_notes(record.quick_notes),
|
||||||
created_at: record.created_at,
|
created_at: record.created_at,
|
||||||
updated_at: record.updated_at,
|
updated_at: record.updated_at,
|
||||||
}
|
}
|
||||||
|
|
@ -79,6 +101,7 @@ impl SqlBrewRepository {
|
||||||
filter_paper_id: record.filter_paper_id.map(GearId::new),
|
filter_paper_id: record.filter_paper_id.map(GearId::new),
|
||||||
water_volume: record.water_volume,
|
water_volume: record.water_volume,
|
||||||
water_temp: record.water_temp,
|
water_temp: record.water_temp,
|
||||||
|
quick_notes: Self::decode_quick_notes(record.quick_notes),
|
||||||
created_at: record.created_at,
|
created_at: record.created_at,
|
||||||
updated_at: record.updated_at,
|
updated_at: record.updated_at,
|
||||||
},
|
},
|
||||||
|
|
@ -135,9 +158,9 @@ impl BrewRepository for SqlBrewRepository {
|
||||||
|
|
||||||
// Insert the brew
|
// Insert the brew
|
||||||
let insert_query = r"
|
let insert_query = r"
|
||||||
INSERT INTO brews (bag_id, coffee_weight, grinder_id, grind_setting, brewer_id, filter_paper_id, water_volume, water_temp)
|
INSERT INTO brews (bag_id, coffee_weight, grinder_id, grind_setting, brewer_id, filter_paper_id, water_volume, water_temp, quick_notes)
|
||||||
VALUES (?, ?, ?, ?, ?, ?, ?, ?)
|
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)
|
||||||
RETURNING id, bag_id, coffee_weight, grinder_id, grind_setting, brewer_id, filter_paper_id, water_volume, water_temp, created_at, updated_at
|
RETURNING id, bag_id, coffee_weight, grinder_id, grind_setting, brewer_id, filter_paper_id, water_volume, water_temp, quick_notes, created_at, updated_at
|
||||||
";
|
";
|
||||||
|
|
||||||
let record = query_as::<_, BrewRecord>(insert_query)
|
let record = query_as::<_, BrewRecord>(insert_query)
|
||||||
|
|
@ -152,6 +175,7 @@ impl BrewRepository for SqlBrewRepository {
|
||||||
)
|
)
|
||||||
.bind(brew.water_volume)
|
.bind(brew.water_volume)
|
||||||
.bind(brew.water_temp)
|
.bind(brew.water_temp)
|
||||||
|
.bind(Self::encode_quick_notes(&brew.quick_notes))
|
||||||
.fetch_one(&mut *tx)
|
.fetch_one(&mut *tx)
|
||||||
.await
|
.await
|
||||||
.map_err(|err| RepositoryError::unexpected(err.to_string()))?;
|
.map_err(|err| RepositoryError::unexpected(err.to_string()))?;
|
||||||
|
|
@ -165,7 +189,7 @@ impl BrewRepository for SqlBrewRepository {
|
||||||
|
|
||||||
async fn get(&self, id: BrewId) -> Result<Brew, RepositoryError> {
|
async fn get(&self, id: BrewId) -> Result<Brew, RepositoryError> {
|
||||||
let query = r"
|
let query = r"
|
||||||
SELECT id, bag_id, coffee_weight, grinder_id, grind_setting, brewer_id, filter_paper_id, water_volume, water_temp, created_at, updated_at
|
SELECT id, bag_id, coffee_weight, grinder_id, grind_setting, brewer_id, filter_paper_id, water_volume, water_temp, quick_notes, created_at, updated_at
|
||||||
FROM brews
|
FROM brews
|
||||||
WHERE id = ?
|
WHERE id = ?
|
||||||
";
|
";
|
||||||
|
|
@ -263,6 +287,7 @@ struct BrewRecord {
|
||||||
filter_paper_id: Option<i64>,
|
filter_paper_id: Option<i64>,
|
||||||
water_volume: i32,
|
water_volume: i32,
|
||||||
water_temp: f64,
|
water_temp: f64,
|
||||||
|
quick_notes: Option<String>,
|
||||||
created_at: DateTime<Utc>,
|
created_at: DateTime<Utc>,
|
||||||
updated_at: DateTime<Utc>,
|
updated_at: DateTime<Utc>,
|
||||||
}
|
}
|
||||||
|
|
@ -278,6 +303,7 @@ struct BrewWithDetailsRecord {
|
||||||
filter_paper_id: Option<i64>,
|
filter_paper_id: Option<i64>,
|
||||||
water_volume: i32,
|
water_volume: i32,
|
||||||
water_temp: f64,
|
water_temp: f64,
|
||||||
|
quick_notes: Option<String>,
|
||||||
created_at: DateTime<Utc>,
|
created_at: DateTime<Utc>,
|
||||||
updated_at: DateTime<Utc>,
|
updated_at: DateTime<Utc>,
|
||||||
roast_name: String,
|
roast_name: String,
|
||||||
|
|
|
||||||
|
|
@ -3,6 +3,7 @@ use clap::{Args, Subcommand};
|
||||||
|
|
||||||
use super::macros::{define_delete_command, define_get_command};
|
use super::macros::{define_delete_command, define_get_command};
|
||||||
use super::print_json;
|
use super::print_json;
|
||||||
|
use crate::domain::brews::QuickNote;
|
||||||
use crate::domain::ids::{BagId, BrewId, GearId};
|
use crate::domain::ids::{BagId, BrewId, GearId};
|
||||||
use crate::infrastructure::client::BrewlogClient;
|
use crate::infrastructure::client::BrewlogClient;
|
||||||
|
|
||||||
|
|
@ -60,9 +61,19 @@ pub struct AddBrewCommand {
|
||||||
/// Water temperature in Celsius
|
/// Water temperature in Celsius
|
||||||
#[arg(long, default_value = "91.0")]
|
#[arg(long, default_value = "91.0")]
|
||||||
pub water_temp: f64,
|
pub water_temp: f64,
|
||||||
|
|
||||||
|
/// Quick notes (comma-separated: good,too-fast,too-slow,too-hot,under-extracted,over-extracted)
|
||||||
|
#[arg(long, value_delimiter = ',')]
|
||||||
|
pub quick_notes: Vec<String>,
|
||||||
}
|
}
|
||||||
|
|
||||||
pub async fn add_brew(client: &BrewlogClient, command: AddBrewCommand) -> Result<()> {
|
pub async fn add_brew(client: &BrewlogClient, command: AddBrewCommand) -> Result<()> {
|
||||||
|
let quick_notes: Vec<QuickNote> = command
|
||||||
|
.quick_notes
|
||||||
|
.iter()
|
||||||
|
.filter_map(|s| QuickNote::from_str_value(s))
|
||||||
|
.collect();
|
||||||
|
|
||||||
let brew = client
|
let brew = client
|
||||||
.brews()
|
.brews()
|
||||||
.create(
|
.create(
|
||||||
|
|
@ -74,6 +85,7 @@ pub async fn add_brew(client: &BrewlogClient, command: AddBrewCommand) -> Result
|
||||||
command.filter_paper_id.map(GearId::new),
|
command.filter_paper_id.map(GearId::new),
|
||||||
command.water_volume,
|
command.water_volume,
|
||||||
command.water_temp,
|
command.water_temp,
|
||||||
|
quick_notes,
|
||||||
)
|
)
|
||||||
.await?;
|
.await?;
|
||||||
print_json(&brew)
|
print_json(&brew)
|
||||||
|
|
|
||||||
|
|
@ -2,8 +2,9 @@ use askama::Template;
|
||||||
|
|
||||||
use super::views::{
|
use super::views::{
|
||||||
BagOptionView, BagView, BrewDefaultsView, BrewView, CafeOptionView, CafeView, CupView,
|
BagOptionView, BagView, BrewDefaultsView, BrewView, CafeOptionView, CafeView, CupView,
|
||||||
GearOptionView, GearView, ListNavigator, NearbyCafeView, Paginated, RoastOptionView, RoastView,
|
GearOptionView, GearView, ListNavigator, NearbyCafeView, Paginated, QuickNoteView,
|
||||||
RoasterOptionView, RoasterView, StatsView, TimelineEventView, TimelineMonthView,
|
RoastOptionView, RoastView, RoasterOptionView, RoasterView, StatsView, TimelineEventView,
|
||||||
|
TimelineMonthView,
|
||||||
};
|
};
|
||||||
use crate::domain::bags::BagSortKey;
|
use crate::domain::bags::BagSortKey;
|
||||||
use crate::domain::brews::BrewSortKey;
|
use crate::domain::brews::BrewSortKey;
|
||||||
|
|
@ -170,6 +171,7 @@ pub struct AddTemplate {
|
||||||
pub filter_paper_options: Vec<GearOptionView>,
|
pub filter_paper_options: Vec<GearOptionView>,
|
||||||
pub cafe_options: Vec<CafeOptionView>,
|
pub cafe_options: Vec<CafeOptionView>,
|
||||||
pub defaults: BrewDefaultsView,
|
pub defaults: BrewDefaultsView,
|
||||||
|
pub quick_note_options: Vec<QuickNoteView>,
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn render_template<T: Template>(template: T) -> Result<String, askama::Error> {
|
pub fn render_template<T: Template>(template: T) -> Result<String, askama::Error> {
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,28 @@
|
||||||
use crate::domain::brews::{Brew, BrewWithDetails};
|
use crate::domain::brews::{Brew, BrewWithDetails, QuickNote};
|
||||||
|
|
||||||
use super::relative_date;
|
use super::relative_date;
|
||||||
|
|
||||||
|
#[derive(Clone)]
|
||||||
|
pub struct QuickNoteView {
|
||||||
|
pub label: String,
|
||||||
|
pub pill_class: &'static str,
|
||||||
|
pub form_value: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<QuickNote> 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)]
|
#[derive(Clone)]
|
||||||
pub struct BrewView {
|
pub struct BrewView {
|
||||||
pub id: String,
|
pub id: String,
|
||||||
|
|
@ -21,6 +42,8 @@ pub struct BrewView {
|
||||||
pub water_volume: String,
|
pub water_volume: String,
|
||||||
pub water_temp: String,
|
pub water_temp: String,
|
||||||
pub ratio: String,
|
pub ratio: String,
|
||||||
|
pub quick_notes: Vec<QuickNoteView>,
|
||||||
|
pub quick_notes_label: String,
|
||||||
pub created_date: String,
|
pub created_date: String,
|
||||||
pub created_time: String,
|
pub created_time: String,
|
||||||
pub relative_date_label: String,
|
pub relative_date_label: String,
|
||||||
|
|
@ -42,6 +65,19 @@ impl BrewView {
|
||||||
"\u{2014}".to_string()
|
"\u{2014}".to_string()
|
||||||
};
|
};
|
||||||
|
|
||||||
|
let quick_notes: Vec<QuickNoteView> = brew
|
||||||
|
.brew
|
||||||
|
.quick_notes
|
||||||
|
.iter()
|
||||||
|
.copied()
|
||||||
|
.map(QuickNoteView::from)
|
||||||
|
.collect();
|
||||||
|
let quick_notes_label = quick_notes
|
||||||
|
.iter()
|
||||||
|
.map(|n| n.label.as_str())
|
||||||
|
.collect::<Vec<_>>()
|
||||||
|
.join(", ");
|
||||||
|
|
||||||
Self {
|
Self {
|
||||||
id: brew.brew.id.to_string(),
|
id: brew.brew.id.to_string(),
|
||||||
bag_id: brew.brew.bag_id.into_inner(),
|
bag_id: brew.brew.bag_id.into_inner(),
|
||||||
|
|
@ -63,6 +99,8 @@ impl BrewView {
|
||||||
water_volume: format!("{}ml", brew.brew.water_volume),
|
water_volume: format!("{}ml", brew.brew.water_volume),
|
||||||
water_temp: format!("{:.1}\u{00B0}C", brew.brew.water_temp),
|
water_temp: format!("{:.1}\u{00B0}C", brew.brew.water_temp),
|
||||||
ratio,
|
ratio,
|
||||||
|
quick_notes,
|
||||||
|
quick_notes_label,
|
||||||
created_date: brew.brew.created_at.format("%Y-%m-%d").to_string(),
|
created_date: brew.brew.created_at.format("%Y-%m-%d").to_string(),
|
||||||
created_time: brew.brew.created_at.format("%H:%M").to_string(),
|
created_time: brew.brew.created_at.format("%H:%M").to_string(),
|
||||||
relative_date_label: relative_date(brew.brew.created_at),
|
relative_date_label: relative_date(brew.brew.created_at),
|
||||||
|
|
|
||||||
|
|
@ -8,7 +8,7 @@ mod roasts;
|
||||||
mod timeline;
|
mod timeline;
|
||||||
|
|
||||||
pub use bags::{BagOptionView, BagView};
|
pub use bags::{BagOptionView, BagView};
|
||||||
pub use brews::{BrewDefaultsView, BrewView};
|
pub use brews::{BrewDefaultsView, BrewView, QuickNoteView};
|
||||||
pub use cafes::{CafeOptionView, CafeView, NearbyCafeView};
|
pub use cafes::{CafeOptionView, CafeView, NearbyCafeView};
|
||||||
pub use cups::CupView;
|
pub use cups::CupView;
|
||||||
pub use gear::{GearOptionView, GearView};
|
pub use gear::{GearOptionView, GearView};
|
||||||
|
|
|
||||||
|
|
@ -81,6 +81,12 @@
|
||||||
data-signals:_user-lat="0"
|
data-signals:_user-lat="0"
|
||||||
data-signals:_user-lng="0"
|
data-signals:_user-lng="0"
|
||||||
data-signals:_reviewing-cafe="false"
|
data-signals:_reviewing-cafe="false"
|
||||||
|
data-signals:_qn-good="false"
|
||||||
|
data-signals:_qn-too-fast="false"
|
||||||
|
data-signals:_qn-too-slow="false"
|
||||||
|
data-signals:_qn-too-hot="false"
|
||||||
|
data-signals:_qn-under-extracted="false"
|
||||||
|
data-signals:_qn-over-extracted="false"
|
||||||
>
|
>
|
||||||
<h2 class="text-lg font-semibold text-stone-800 mb-3">Manual Add</h2>
|
<h2 class="text-lg font-semibold text-stone-800 mb-3">Manual Add</h2>
|
||||||
|
|
||||||
|
|
@ -341,6 +347,32 @@
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
<!-- Quick Notes -->
|
||||||
|
<div>
|
||||||
|
<h4 class="text-xs font-semibold text-stone-500 uppercase tracking-wide mb-3">Quick Notes</h4>
|
||||||
|
<div class="flex flex-wrap gap-2">
|
||||||
|
<button type="button"
|
||||||
|
data-on:click="$_qnGood = !$_qnGood"
|
||||||
|
data-attr:class="$_qnGood ? 'pill pill-success cursor-pointer select-none transition' : 'pill pill-muted cursor-pointer select-none transition'">Good</button>
|
||||||
|
<button type="button"
|
||||||
|
data-on:click="$_qnTooFast = !$_qnTooFast"
|
||||||
|
data-attr:class="$_qnTooFast ? 'pill pill-warning cursor-pointer select-none transition' : 'pill pill-muted cursor-pointer select-none transition'">Too Fast</button>
|
||||||
|
<button type="button"
|
||||||
|
data-on:click="$_qnTooSlow = !$_qnTooSlow"
|
||||||
|
data-attr:class="$_qnTooSlow ? 'pill pill-warning cursor-pointer select-none transition' : 'pill pill-muted cursor-pointer select-none transition'">Too Slow</button>
|
||||||
|
<button type="button"
|
||||||
|
data-on:click="$_qnTooHot = !$_qnTooHot"
|
||||||
|
data-attr:class="$_qnTooHot ? 'pill pill-warning cursor-pointer select-none transition' : 'pill pill-muted cursor-pointer select-none transition'">Too Hot</button>
|
||||||
|
<button type="button"
|
||||||
|
data-on:click="$_qnUnderExtracted = !$_qnUnderExtracted"
|
||||||
|
data-attr:class="$_qnUnderExtracted ? 'pill pill-warning cursor-pointer select-none transition' : 'pill pill-muted cursor-pointer select-none transition'">Under Extracted</button>
|
||||||
|
<button type="button"
|
||||||
|
data-on:click="$_qnOverExtracted = !$_qnOverExtracted"
|
||||||
|
data-attr:class="$_qnOverExtracted ? 'pill pill-warning cursor-pointer select-none transition' : 'pill pill-muted cursor-pointer select-none transition'">Over Extracted</button>
|
||||||
|
</div>
|
||||||
|
<input type="hidden" name="quick_notes"
|
||||||
|
data-attr:value="[$_qnGood && 'good', $_qnTooFast && 'too-fast', $_qnTooSlow && 'too-slow', $_qnTooHot && 'too-hot', $_qnUnderExtracted && 'under-extracted', $_qnOverExtracted && 'over-extracted'].filter(Boolean).join(',')" />
|
||||||
|
</div>
|
||||||
<div class="flex items-center justify-end">
|
<div class="flex items-center justify-end">
|
||||||
<button type="submit" class="rounded-md bg-accent px-4 py-2 text-sm font-semibold text-accent-text transition hover:bg-accent-hover">
|
<button type="submit" class="rounded-md bg-accent px-4 py-2 text-sm font-semibold text-accent-text transition hover:bg-accent-hover">
|
||||||
Log Brew
|
Log Brew
|
||||||
|
|
|
||||||
|
|
@ -94,6 +94,13 @@
|
||||||
<p class="truncate"><span class="font-medium text-stone-800">{{ brew.grind_setting }}</span> <span class="text-stone-500">· {{ brew.grinder_name }}</span></p>
|
<p class="truncate"><span class="font-medium text-stone-800">{{ brew.grind_setting }}</span> <span class="text-stone-500">· {{ brew.grinder_name }}</span></p>
|
||||||
<p class="truncate"><span class="font-medium text-stone-800">{{ brew.brewer_name }}</span>{% if let Some(fp) = brew.filter_paper_name %} <span class="text-stone-500">· {{ fp }}</span>{% endif %}</p>
|
<p class="truncate"><span class="font-medium text-stone-800">{{ brew.brewer_name }}</span>{% if let Some(fp) = brew.filter_paper_name %} <span class="text-stone-500">· {{ fp }}</span>{% endif %}</p>
|
||||||
</div>
|
</div>
|
||||||
|
{% if !brew.quick_notes.is_empty() %}
|
||||||
|
<div class="mt-2 flex flex-wrap gap-1">
|
||||||
|
{% for qn in brew.quick_notes %}
|
||||||
|
<span class="{{ qn.pill_class }}">{{ qn.label }}</span>
|
||||||
|
{% endfor %}
|
||||||
|
</div>
|
||||||
|
{% endif %}
|
||||||
{% 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">
|
||||||
<form
|
<form
|
||||||
|
|
|
||||||
|
|
@ -29,6 +29,7 @@
|
||||||
<th scope="col" class="px-4 py-3">Grind</th>
|
<th scope="col" class="px-4 py-3">Grind</th>
|
||||||
<th scope="col" class="px-4 py-3">Recipe</th>
|
<th scope="col" class="px-4 py-3">Recipe</th>
|
||||||
<th scope="col" class="px-4 py-3">Brewer</th>
|
<th scope="col" class="px-4 py-3">Brewer</th>
|
||||||
|
<th scope="col" class="px-4 py-3">Notes</th>
|
||||||
{% if is_authenticated %}
|
{% if is_authenticated %}
|
||||||
<th scope="col" class="px-4 py-3 text-right">Actions</th>
|
<th scope="col" class="px-4 py-3 text-right">Actions</th>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
|
@ -66,6 +67,13 @@
|
||||||
{% if let Some(fp_name) = brew.filter_paper_name %}
|
{% if let Some(fp_name) = brew.filter_paper_name %}
|
||||||
<td data-label="Filter" class="px-4 py-3 whitespace-nowrap md:hidden">{{ fp_name }}</td>
|
<td data-label="Filter" class="px-4 py-3 whitespace-nowrap md:hidden">{{ fp_name }}</td>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
<td data-label="Notes" class="px-4 py-3 text-xs text-stone-500">
|
||||||
|
{% if !brew.quick_notes_label.is_empty() %}
|
||||||
|
{{ brew.quick_notes_label }}
|
||||||
|
{% else %}
|
||||||
|
<span class="text-stone-400">—</span>
|
||||||
|
{% endif %}
|
||||||
|
</td>
|
||||||
{% 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">
|
||||||
|
|
|
||||||
|
|
@ -201,6 +201,7 @@ async fn populate_test_data(db: &TestDb) -> (Roaster, Roast, Bag, Gear, Gear, Ge
|
||||||
filter_paper_id: Some(filter_paper.id),
|
filter_paper_id: Some(filter_paper.id),
|
||||||
water_volume: 250,
|
water_volume: 250,
|
||||||
water_temp: 93.5,
|
water_temp: 93.5,
|
||||||
|
quick_notes: Vec::new(),
|
||||||
})
|
})
|
||||||
.await
|
.await
|
||||||
.expect("failed to create brew");
|
.expect("failed to create brew");
|
||||||
|
|
|
||||||
|
|
@ -25,6 +25,7 @@ async fn creating_a_brew_returns_201_for_valid_data() {
|
||||||
filter_paper_id: None,
|
filter_paper_id: None,
|
||||||
water_volume: 250,
|
water_volume: 250,
|
||||||
water_temp: 92.0,
|
water_temp: 92.0,
|
||||||
|
quick_notes: Vec::new(),
|
||||||
};
|
};
|
||||||
|
|
||||||
// Act
|
// Act
|
||||||
|
|
@ -71,6 +72,7 @@ async fn creating_a_brew_with_filter_paper_returns_201() {
|
||||||
filter_paper_id: Some(filter_paper.id),
|
filter_paper_id: Some(filter_paper.id),
|
||||||
water_volume: 250,
|
water_volume: 250,
|
||||||
water_temp: 92.0,
|
water_temp: 92.0,
|
||||||
|
quick_notes: Vec::new(),
|
||||||
};
|
};
|
||||||
|
|
||||||
// Act
|
// Act
|
||||||
|
|
@ -109,6 +111,7 @@ async fn creating_a_brew_deducts_from_bag_remaining() {
|
||||||
filter_paper_id: None,
|
filter_paper_id: None,
|
||||||
water_volume: 250,
|
water_volume: 250,
|
||||||
water_temp: 92.0,
|
water_temp: 92.0,
|
||||||
|
quick_notes: Vec::new(),
|
||||||
};
|
};
|
||||||
|
|
||||||
// Act
|
// Act
|
||||||
|
|
@ -151,6 +154,7 @@ async fn creating_a_brew_fails_if_insufficient_coffee_in_bag() {
|
||||||
filter_paper_id: None,
|
filter_paper_id: None,
|
||||||
water_volume: 250,
|
water_volume: 250,
|
||||||
water_temp: 92.0,
|
water_temp: 92.0,
|
||||||
|
quick_notes: Vec::new(),
|
||||||
};
|
};
|
||||||
|
|
||||||
// Act
|
// Act
|
||||||
|
|
@ -217,6 +221,7 @@ async fn listing_brews_returns_200_and_enriched_data() {
|
||||||
filter_paper_id: Some(filter_paper.id),
|
filter_paper_id: Some(filter_paper.id),
|
||||||
water_volume: 250,
|
water_volume: 250,
|
||||||
water_temp: 92.0,
|
water_temp: 92.0,
|
||||||
|
quick_notes: Vec::new(),
|
||||||
};
|
};
|
||||||
|
|
||||||
client
|
client
|
||||||
|
|
@ -270,6 +275,7 @@ async fn listing_brews_without_filter_paper_returns_none() {
|
||||||
filter_paper_id: None,
|
filter_paper_id: None,
|
||||||
water_volume: 255,
|
water_volume: 255,
|
||||||
water_temp: 88.0,
|
water_temp: 88.0,
|
||||||
|
quick_notes: Vec::new(),
|
||||||
};
|
};
|
||||||
|
|
||||||
client
|
client
|
||||||
|
|
@ -318,6 +324,7 @@ async fn listing_brews_with_bag_filter_returns_filtered_results() {
|
||||||
filter_paper_id: None,
|
filter_paper_id: None,
|
||||||
water_volume: 250,
|
water_volume: 250,
|
||||||
water_temp: 92.0,
|
water_temp: 92.0,
|
||||||
|
quick_notes: Vec::new(),
|
||||||
};
|
};
|
||||||
|
|
||||||
client
|
client
|
||||||
|
|
@ -338,6 +345,7 @@ async fn listing_brews_with_bag_filter_returns_filtered_results() {
|
||||||
filter_paper_id: None,
|
filter_paper_id: None,
|
||||||
water_volume: 255,
|
water_volume: 255,
|
||||||
water_temp: 88.0,
|
water_temp: 88.0,
|
||||||
|
quick_notes: Vec::new(),
|
||||||
};
|
};
|
||||||
|
|
||||||
client
|
client
|
||||||
|
|
@ -383,6 +391,7 @@ async fn getting_a_brew_returns_200_for_valid_id() {
|
||||||
filter_paper_id: None,
|
filter_paper_id: None,
|
||||||
water_volume: 250,
|
water_volume: 250,
|
||||||
water_temp: 92.0,
|
water_temp: 92.0,
|
||||||
|
quick_notes: Vec::new(),
|
||||||
};
|
};
|
||||||
|
|
||||||
let create_response = client
|
let create_response = client
|
||||||
|
|
@ -432,6 +441,7 @@ async fn deleting_a_brew_returns_204() {
|
||||||
filter_paper_id: None,
|
filter_paper_id: None,
|
||||||
water_volume: 250,
|
water_volume: 250,
|
||||||
water_temp: 92.0,
|
water_temp: 92.0,
|
||||||
|
quick_notes: Vec::new(),
|
||||||
};
|
};
|
||||||
|
|
||||||
let create_response = client
|
let create_response = client
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue