From 91a9900c24d09a2b402a9976d7c3d84977d256b2 Mon Sep 17 00:00:00 2001 From: Jon Seager Date: Wed, 4 Feb 2026 17:41:53 +0000 Subject: [PATCH] feat(scan): optionally open a bag when scanning coffee Add "Open a bag of this coffee" checkbox (default: checked) to the scan form with a configurable amount field (default: 250g). When enabled, creates a bag and timeline event alongside the roaster/roast. --- src/application/routes/scan.rs | 49 ++++++++++++++++++++++++++++++++++ templates/home.html | 16 ++++++++++- 2 files changed, 64 insertions(+), 1 deletion(-) diff --git a/src/application/routes/scan.rs b/src/application/routes/scan.rs index 7ff6540..16c7c1b 100644 --- a/src/application/routes/scan.rs +++ b/src/application/routes/scan.rs @@ -9,9 +9,11 @@ use crate::application::errors::{ApiError, AppError}; use crate::application::routes::roasts::TastingNotesInput; use crate::application::routes::support::{FlexiblePayload, is_datastar_request}; use crate::application::server::AppState; +use crate::domain::bags::NewBag; use crate::domain::errors::RepositoryError; use crate::domain::roasters::NewRoaster; use crate::domain::roasts::NewRoast; +use crate::domain::timeline::{NewTimelineEvent, TimelineEventDetail}; use crate::infrastructure::ai::{self, ExtractionInput}; #[tracing::instrument(skip(state, _auth_user, headers, payload))] @@ -115,6 +117,10 @@ pub(crate) struct BagScanSubmission { process: String, #[serde(default = "default_tasting_notes")] tasting_notes: TastingNotesInput, + #[serde(default)] + open_bag: Option, + #[serde(default)] + bag_amount: Option, } #[derive(Debug, Serialize)] @@ -175,6 +181,7 @@ async fn extract_into_submission( Ok(()) } +#[allow(clippy::too_many_lines)] #[tracing::instrument(skip(state, _auth_user, headers, payload))] pub(crate) async fn submit_scan( State(state): State, @@ -262,6 +269,48 @@ pub(crate) async fn submit_scan( .await .map_err(AppError::from)?; + // Optionally create a bag + let wants_bag = submission + .open_bag + .as_deref() + .is_some_and(|v| v == "true" || v == "on"); + if wants_bag { + let amount = submission.bag_amount.unwrap_or(250.0); + let new_bag = NewBag { + roast_id: roast.id, + roast_date: None, + amount, + }; + let bag = state + .bag_repo + .insert(new_bag) + .await + .map_err(AppError::from)?; + + let event = NewTimelineEvent { + entity_type: "bag".to_string(), + entity_id: bag.id.into_inner(), + action: "added".to_string(), + occurred_at: chrono::Utc::now(), + title: roast.name.clone(), + details: vec![ + TimelineEventDetail { + label: "Roaster".to_string(), + value: roaster.name.clone(), + }, + TimelineEventDetail { + label: "Amount".to_string(), + value: format!("{}g", bag.amount), + }, + ], + tasting_notes: vec![], + slug: Some(roast.slug.clone()), + roaster_slug: Some(roaster.slug.clone()), + brew_data: None, + }; + let _ = state.timeline_repo.insert(event).await; + } + let redirect = format!("/roasters/{}/roasts/{}", roaster.slug, roast.slug); let roast_id = roast.id.into_inner(); diff --git a/templates/home.html b/templates/home.html index 55144be..79b1452 100644 --- a/templates/home.html +++ b/templates/home.html @@ -46,6 +46,8 @@ data-signals:_producer="''" data-signals:_process="''" data-signals:_tasting-notes="''" + data-signals:_open-bag="true" + data-signals:_bag-amount="250" >

Scan Bag

@@ -89,7 +91,7 @@

Roaster

@@ -143,6 +145,18 @@
+
+ +
+ +
+