From 09a629db9ac2dd0404a7ace973d205e79c27b559 Mon Sep 17 00:00:00 2001 From: Jon Seager Date: Tue, 3 Feb 2026 14:01:41 +0000 Subject: [PATCH] feat(brews): pre-populate Add Brew form with last recorded brew values - Add BrewDefaultsView to carry last brew's values to the template - Extract form data loading into load_brew_form_data() helper - Use defaults for signal initialization and dropdown pre-selection - Retain submitted values after form submission instead of resetting --- src/application/routes/brews.rs | 176 ++++++++++++++++++------------ src/presentation/web/templates.rs | 5 +- src/presentation/web/views.rs | 46 +++++++- templates/brews.html | 12 +- 4 files changed, 162 insertions(+), 77 deletions(-) diff --git a/src/application/routes/brews.rs b/src/application/routes/brews.rs index 15a3732..12a821b 100644 --- a/src/application/routes/brews.rs +++ b/src/application/routes/brews.rs @@ -16,11 +16,11 @@ use crate::domain::bags::BagFilter; use crate::domain::brews::{BrewFilter, BrewSortKey, BrewWithDetails, NewBrew}; use crate::domain::gear::{GearCategory, GearFilter, GearSortKey}; use crate::domain::ids::{BagId, BrewId, GearId}; -use crate::domain::listing::{ListRequest, SortDirection}; +use crate::domain::listing::{ListRequest, PageSize, SortDirection}; use crate::domain::timeline::{NewTimelineEvent, TimelineBrewData, TimelineEventDetail}; use crate::presentation::web::templates::{BrewListTemplate, BrewsTemplate}; use crate::presentation::web::views::{ - BagOptionView, BrewView, GearOptionView, ListNavigator, Paginated, + BagOptionView, BrewDefaultsView, BrewView, GearOptionView, ListNavigator, Paginated, }; const BREW_PAGE_PATH: &str = "/brews"; @@ -31,6 +31,104 @@ struct BrewPageData { navigator: ListNavigator, } +struct BrewFormData { + bag_options: Vec, + grinder_options: Vec, + brewer_options: Vec, + filter_paper_options: Vec, + defaults: BrewDefaultsView, +} + +async fn load_brew_form_data(state: &AppState) -> Result { + let open_bags_request = ListRequest::show_all( + crate::domain::bags::BagSortKey::RoastDate, + SortDirection::Desc, + ); + let open_bags = state + .bag_repo + .list(BagFilter::open(), &open_bags_request, None) + .await + .map_err(AppError::from)?; + let bag_options: Vec = open_bags + .items + .into_iter() + .map(BagOptionView::from) + .collect(); + + let gear_request = ListRequest::show_all(GearSortKey::Make, SortDirection::Asc); + + let grinders = state + .gear_repo + .list( + GearFilter::for_category(GearCategory::Grinder), + &gear_request, + None, + ) + .await + .map_err(AppError::from)?; + let grinder_options: Vec = grinders + .items + .into_iter() + .map(GearOptionView::from) + .collect(); + + let brewers = state + .gear_repo + .list( + GearFilter::for_category(GearCategory::Brewer), + &gear_request, + None, + ) + .await + .map_err(AppError::from)?; + let brewer_options: Vec = brewers + .items + .into_iter() + .map(GearOptionView::from) + .collect(); + + let filter_papers = state + .gear_repo + .list( + GearFilter::for_category(GearCategory::FilterPaper), + &gear_request, + None, + ) + .await + .map_err(AppError::from)?; + let filter_paper_options: Vec = filter_papers + .items + .into_iter() + .map(GearOptionView::from) + .collect(); + + let last_brew_request = ListRequest::new( + 1, + PageSize::Limited(1), + BrewSortKey::CreatedAt, + SortDirection::Desc, + ); + let last_brew_page = state + .brew_repo + .list(BrewFilter::all(), &last_brew_request, None) + .await + .map_err(AppError::from)?; + let defaults = last_brew_page + .items + .into_iter() + .next() + .map(|b| BrewDefaultsView::from(b.brew)) + .unwrap_or_default(); + + Ok(BrewFormData { + bag_options, + grinder_options, + brewer_options, + filter_paper_options, + defaults, + }) +} + #[tracing::instrument(skip(state))] async fn load_brew_page( state: &AppState, @@ -71,72 +169,13 @@ pub(crate) async fn brews_page( .map_err(map_app_error); } - // Load open bags for the form dropdown - let open_bags_request = ListRequest::show_all( - crate::domain::bags::BagSortKey::RoastDate, - SortDirection::Desc, - ); - let open_bags = state - .bag_repo - .list(BagFilter::open(), &open_bags_request, None) - .await - .map_err(|err| map_app_error(AppError::from(err)))?; - let bag_options: Vec = open_bags - .items - .into_iter() - .map(BagOptionView::from) - .collect(); - - // Load grinders for dropdown - let grinder_request = ListRequest::show_all(GearSortKey::Make, SortDirection::Asc); - let grinders = state - .gear_repo - .list( - GearFilter::for_category(GearCategory::Grinder), - &grinder_request, - None, - ) - .await - .map_err(|err| map_app_error(AppError::from(err)))?; - let grinder_options: Vec = grinders - .items - .into_iter() - .map(GearOptionView::from) - .collect(); - - // Load brewers for dropdown - let brewer_request = ListRequest::show_all(GearSortKey::Make, SortDirection::Asc); - let brewers = state - .gear_repo - .list( - GearFilter::for_category(GearCategory::Brewer), - &brewer_request, - None, - ) - .await - .map_err(|err| map_app_error(AppError::from(err)))?; - let brewer_options: Vec = brewers - .items - .into_iter() - .map(GearOptionView::from) - .collect(); - - // Load filter papers for dropdown (optional gear) - let filter_paper_request = ListRequest::show_all(GearSortKey::Make, SortDirection::Asc); - let filter_papers = state - .gear_repo - .list( - GearFilter::for_category(GearCategory::FilterPaper), - &filter_paper_request, - None, - ) - .await - .map_err(|err| map_app_error(AppError::from(err)))?; - let filter_paper_options: Vec = filter_papers - .items - .into_iter() - .map(GearOptionView::from) - .collect(); + let BrewFormData { + bag_options, + grinder_options, + brewer_options, + filter_paper_options, + defaults, + } = load_brew_form_data(&state).await.map_err(map_app_error)?; let BrewPageData { brews, navigator } = load_brew_page(&state, request, search.as_deref()) .await @@ -152,6 +191,7 @@ pub(crate) async fn brews_page( grinder_options, brewer_options, filter_paper_options, + defaults, navigator, }; diff --git a/src/presentation/web/templates.rs b/src/presentation/web/templates.rs index db79ab1..691f2ce 100644 --- a/src/presentation/web/templates.rs +++ b/src/presentation/web/templates.rs @@ -1,8 +1,8 @@ use askama::Template; use super::views::{ - BagOptionView, BagView, BrewView, GearOptionView, GearView, ListNavigator, Paginated, - RoastView, RoasterOptionView, RoasterView, TimelineEventView, TimelineMonthView, + BagOptionView, BagView, BrewDefaultsView, BrewView, GearOptionView, GearView, ListNavigator, + Paginated, RoastView, RoasterOptionView, RoasterView, TimelineEventView, TimelineMonthView, }; use crate::domain::bags::BagSortKey; use crate::domain::brews::BrewSortKey; @@ -136,6 +136,7 @@ pub struct BrewsTemplate { pub grinder_options: Vec, pub brewer_options: Vec, pub filter_paper_options: Vec, + pub defaults: BrewDefaultsView, pub navigator: ListNavigator, } diff --git a/src/presentation/web/views.rs b/src/presentation/web/views.rs index afffffc..c490ee4 100644 --- a/src/presentation/web/views.rs +++ b/src/presentation/web/views.rs @@ -1,5 +1,5 @@ use crate::domain::bags::BagWithRoast; -use crate::domain::brews::BrewWithDetails; +use crate::domain::brews::{Brew, BrewWithDetails}; use crate::domain::gear::Gear; use crate::domain::listing::{DEFAULT_PAGE_SIZE, ListRequest, Page, PageSize, SortKey}; use crate::domain::roasters::Roaster; @@ -756,3 +756,47 @@ impl From for GearOptionView { } } } + +pub struct BrewDefaultsView { + pub bag_id: String, + pub grinder_id: String, + pub brewer_id: String, + pub filter_paper_id: String, + pub coffee_weight: f64, + pub grind_setting: f64, + pub water_volume: i32, + pub water_temp: f64, +} + +impl Default for BrewDefaultsView { + fn default() -> Self { + Self { + bag_id: String::new(), + grinder_id: String::new(), + brewer_id: String::new(), + filter_paper_id: String::new(), + coffee_weight: 15.0, + grind_setting: 6.0, + water_volume: 250, + water_temp: 91.0, + } + } +} + +impl From for BrewDefaultsView { + fn from(brew: Brew) -> Self { + Self { + bag_id: brew.bag_id.to_string(), + grinder_id: brew.grinder_id.to_string(), + brewer_id: brew.brewer_id.to_string(), + filter_paper_id: brew + .filter_paper_id + .map(|id| id.to_string()) + .unwrap_or_default(), + coffee_weight: brew.coffee_weight, + grind_setting: brew.grind_setting, + water_volume: brew.water_volume, + water_temp: brew.water_temp, + } + } +} diff --git a/templates/brews.html b/templates/brews.html index b043c01..06b84e2 100644 --- a/templates/brews.html +++ b/templates/brews.html @@ -1,5 +1,5 @@ {% extends "base.html" %} {% block title %}Brewlog ยท Brews{% endblock %} {% block content %} -
+

Brews

@@ -53,7 +53,7 @@ class="mt-4 flex flex-col gap-4 overflow-hidden" data-on:submit="$_isSubmitting = true; @post('/api/v1/brews?{{ navigator.query_for_page(1) }}', {contentType: 'form', responseOverrides: {selector: '#brew-list', mode: 'replace'}})" data-ref="_form" - data-on:datastar-fetch="evt.detail.type === 'finished' && $_isSubmitting && ($_showForm = false, $_form && $_form.reset(), $_temp = 91.0, $_grind = 6.0, $_volume = 250, $_weight = 15.0, $_isSubmitting = false)" + data-on:datastar-fetch="evt.detail.type === 'finished' && $_isSubmitting && ($_showForm = false, $_isSubmitting = false)" >
@@ -62,7 +62,7 @@ @@ -72,7 +72,7 @@ Grinder * @@ -82,7 +82,7 @@ Brewer * @@ -93,7 +93,7 @@