brewlog/src/infrastructure/client/brews.rs
Jon Seager 902d52b171
feat(brews): add brew time field
- Add brew_time INTEGER column to brews table (nullable, stored as seconds)
- Display as M:SS format with stepper control (+/-5s) in add form
- Replace ratio with brew time in brew list Recipe column
- Show brew time in brew cards and timeline events
- Add --brew-time CLI flag and update bootstrap script (1:45-2:45 range)
2026-02-07 11:24:07 +00:00

112 lines
3.4 KiB
Rust

use anyhow::{Context, Result};
use chrono::{DateTime, Utc};
use crate::domain::brews::{BrewWithDetails, QuickNote};
use crate::domain::ids::{BagId, BrewId, GearId};
use super::BrewlogClient;
pub struct BrewsClient<'a> {
inner: &'a BrewlogClient,
}
impl<'a> BrewsClient<'a> {
pub(crate) fn new(inner: &'a BrewlogClient) -> Self {
Self { inner }
}
#[allow(clippy::too_many_arguments)]
pub async fn create(
&self,
bag_id: BagId,
coffee_weight: f64,
grinder_id: GearId,
grind_setting: f64,
brewer_id: GearId,
filter_paper_id: Option<GearId>,
water_volume: i32,
water_temp: f64,
quick_notes: Vec<QuickNote>,
brew_time: Option<i32>,
created_at: Option<DateTime<Utc>>,
) -> Result<BrewWithDetails> {
let url = self.inner.endpoint("api/v1/brews")?;
let mut payload = serde_json::json!({
"bag_id": bag_id,
"coffee_weight": coffee_weight,
"grinder_id": grinder_id,
"grind_setting": grind_setting,
"brewer_id": brewer_id,
"water_volume": water_volume,
"water_temp": water_temp,
});
if let Some(fp_id) = filter_paper_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);
}
if let Some(bt) = brew_time {
payload["brew_time"] = serde_json::json!(bt);
}
if let Some(ts) = created_at {
payload["created_at"] = serde_json::json!(ts);
}
let response = self
.inner
.request(reqwest::Method::POST, url)
.json(&payload)
.send()
.await
.context("failed to issue create brew request")?;
self.inner.handle_response(response).await
}
pub async fn list(&self, bag_id: Option<BagId>) -> Result<Vec<BrewWithDetails>> {
let mut url = self.inner.endpoint("api/v1/brews")?;
if let Some(bag_id) = bag_id {
url.query_pairs_mut()
.append_pair("bag_id", &bag_id.to_string());
}
let response = self
.inner
.request(reqwest::Method::GET, url)
.send()
.await
.context("failed to issue list brews request")?;
self.inner.handle_response(response).await
}
pub async fn get(&self, id: BrewId) -> Result<BrewWithDetails> {
let url = self.inner.endpoint(&format!("api/v1/brews/{id}"))?;
let response = self
.inner
.request(reqwest::Method::GET, url)
.send()
.await
.context("failed to issue get brew request")?;
self.inner.handle_response(response).await
}
pub async fn delete(&self, id: BrewId) -> Result<()> {
let url = self.inner.endpoint(&format!("api/v1/brews/{id}"))?;
let response = self
.inner
.request(reqwest::Method::DELETE, url)
.send()
.await
.context("failed to issue delete brew request")?;
if response.status().is_success() {
Ok(())
} else {
Err(self.inner.response_error(response).await)
}
}
}