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
This commit is contained in:
parent
fa0cccade0
commit
09a629db9a
4 changed files with 162 additions and 77 deletions
|
|
@ -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<BrewSortKey>,
|
||||
}
|
||||
|
||||
struct BrewFormData {
|
||||
bag_options: Vec<BagOptionView>,
|
||||
grinder_options: Vec<GearOptionView>,
|
||||
brewer_options: Vec<GearOptionView>,
|
||||
filter_paper_options: Vec<GearOptionView>,
|
||||
defaults: BrewDefaultsView,
|
||||
}
|
||||
|
||||
async fn load_brew_form_data(state: &AppState) -> Result<BrewFormData, AppError> {
|
||||
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<BagOptionView> = 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<GearOptionView> = 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<GearOptionView> = 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<GearOptionView> = 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<BagOptionView> = 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<GearOptionView> = 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<GearOptionView> = 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<GearOptionView> = 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,
|
||||
};
|
||||
|
||||
|
|
|
|||
|
|
@ -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<GearOptionView>,
|
||||
pub brewer_options: Vec<GearOptionView>,
|
||||
pub filter_paper_options: Vec<GearOptionView>,
|
||||
pub defaults: BrewDefaultsView,
|
||||
pub navigator: ListNavigator<BrewSortKey>,
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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<Gear> 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<Brew> 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,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
{% extends "base.html" %} {% block title %}Brewlog · Brews{% endblock %} {% block content %}
|
||||
<section data-signals:_show-form="false" data-signals:_is-submitting="false" data-signals:_temp="91.0" data-signals:_grind="6.0" data-signals:_volume="250" data-signals:_weight="15.0">
|
||||
<section data-signals:_show-form="false" data-signals:_is-submitting="false" data-signals:_temp="{{ defaults.water_temp }}" data-signals:_grind="{{ defaults.grind_setting }}" data-signals:_volume="{{ defaults.water_volume }}" data-signals:_weight="{{ defaults.coffee_weight }}">
|
||||
<header class="flex flex-wrap items-start justify-between gap-4">
|
||||
<div class="flex flex-col gap-2">
|
||||
<h1 class="text-3xl font-semibold">Brews</h1>
|
||||
|
|
@ -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)"
|
||||
>
|
||||
<div class="grid gap-4 sm:grid-cols-2 lg:grid-cols-3">
|
||||
<!-- Bag selector -->
|
||||
|
|
@ -62,7 +62,7 @@
|
|||
<select name="bag_id" required class="input-field">
|
||||
<option value="">Select a bag</option>
|
||||
{% for bag in bag_options %}
|
||||
<option value="{{ bag.id }}">{{ bag.label }}</option>
|
||||
<option value="{{ bag.id }}"{% if bag.id == defaults.bag_id %} selected{% endif %}>{{ bag.label }}</option>
|
||||
{% endfor %}
|
||||
</select>
|
||||
</label>
|
||||
|
|
@ -72,7 +72,7 @@
|
|||
<span class="text-stone-700">Grinder *</span>
|
||||
<select name="grinder_id" required class="input-field">
|
||||
{% for grinder in grinder_options %}
|
||||
<option value="{{ grinder.id }}">{{ grinder.label }}</option>
|
||||
<option value="{{ grinder.id }}"{% if grinder.id == defaults.grinder_id %} selected{% endif %}>{{ grinder.label }}</option>
|
||||
{% endfor %}
|
||||
</select>
|
||||
</label>
|
||||
|
|
@ -82,7 +82,7 @@
|
|||
<span class="text-stone-700">Brewer *</span>
|
||||
<select name="brewer_id" required class="input-field">
|
||||
{% for brewer in brewer_options %}
|
||||
<option value="{{ brewer.id }}">{{ brewer.label }}</option>
|
||||
<option value="{{ brewer.id }}"{% if brewer.id == defaults.brewer_id %} selected{% endif %}>{{ brewer.label }}</option>
|
||||
{% endfor %}
|
||||
</select>
|
||||
</label>
|
||||
|
|
@ -93,7 +93,7 @@
|
|||
<select name="filter_paper_id" class="input-field">
|
||||
<option value="">None</option>
|
||||
{% for fp in filter_paper_options %}
|
||||
<option value="{{ fp.id }}">{{ fp.label }}</option>
|
||||
<option value="{{ fp.id }}"{% if fp.id == defaults.filter_paper_id %} selected{% endif %}>{{ fp.label }}</option>
|
||||
{% endfor %}
|
||||
</select>
|
||||
</label>
|
||||
|
|
|
|||
Loading…
Reference in a new issue