feat(brews): make Brew Again navigate to pre-filled add form
Instead of immediately creating a duplicate brew via inline POST, the Brew Again button now links to the add-brew page with query params that pre-fill all fields (bag, grinder, grind setting, brewer, filter paper, water volume/temp, brew time, quick notes). This lets users review and adjust parameters before brewing.
This commit is contained in:
parent
51f8122201
commit
df4469ff69
6 changed files with 102 additions and 56 deletions
|
|
@ -278,9 +278,7 @@ pub(crate) async fn create_brew(
|
|||
Ok(response)
|
||||
}
|
||||
} else if matches!(source, PayloadSource::Form) {
|
||||
let target =
|
||||
ListNavigator::new(BREW_PAGE_PATH, BREW_FRAGMENT_PATH, request, search).page_href(1);
|
||||
Ok(Redirect::to(&target).into_response())
|
||||
Ok(Redirect::to("/").into_response())
|
||||
} else {
|
||||
Ok((StatusCode::CREATED, Json(enriched)).into_response())
|
||||
}
|
||||
|
|
|
|||
|
|
@ -49,6 +49,16 @@ pub(crate) struct AddQuery {
|
|||
#[serde(rename = "type", default = "default_type")]
|
||||
entity_type: String,
|
||||
bag_id: Option<String>,
|
||||
// Brew-again overrides
|
||||
coffee_weight: Option<f64>,
|
||||
grinder_id: Option<String>,
|
||||
grind_setting: Option<f64>,
|
||||
brewer_id: Option<String>,
|
||||
filter_paper_id: Option<String>,
|
||||
water_volume: Option<i32>,
|
||||
water_temp: Option<f64>,
|
||||
brew_time: Option<i32>,
|
||||
quick_notes: Option<String>,
|
||||
}
|
||||
|
||||
fn default_type() -> String {
|
||||
|
|
@ -75,6 +85,50 @@ pub(crate) async fn add_page(
|
|||
)
|
||||
.map_err(map_app_error)?;
|
||||
|
||||
let mut defaults = brew_form.defaults;
|
||||
|
||||
// Apply brew-again query param overrides
|
||||
if let Some(cw) = query.coffee_weight {
|
||||
defaults.coffee_weight = cw;
|
||||
}
|
||||
if let Some(ref gid) = query.grinder_id {
|
||||
if let Some(opt) = brew_form.grinder_options.iter().find(|o| o.id == *gid) {
|
||||
defaults.grinder_name = opt.label.clone();
|
||||
}
|
||||
defaults.grinder_id = gid.clone();
|
||||
}
|
||||
if let Some(gs) = query.grind_setting {
|
||||
defaults.grind_setting = gs;
|
||||
}
|
||||
if let Some(ref bid) = query.brewer_id {
|
||||
if let Some(opt) = brew_form.brewer_options.iter().find(|o| o.id == *bid) {
|
||||
defaults.brewer_name = opt.label.clone();
|
||||
}
|
||||
defaults.brewer_id = bid.clone();
|
||||
}
|
||||
if let Some(ref fpid) = query.filter_paper_id {
|
||||
if let Some(opt) = brew_form
|
||||
.filter_paper_options
|
||||
.iter()
|
||||
.find(|o| o.id == *fpid)
|
||||
{
|
||||
defaults.filter_paper_name = opt.label.clone();
|
||||
}
|
||||
defaults.filter_paper_id = fpid.clone();
|
||||
}
|
||||
if let Some(wv) = query.water_volume {
|
||||
defaults.water_volume = wv;
|
||||
}
|
||||
if let Some(wt) = query.water_temp {
|
||||
defaults.water_temp = wt;
|
||||
}
|
||||
if let Some(bt) = query.brew_time {
|
||||
defaults.brew_time = Some(bt);
|
||||
}
|
||||
if let Some(ref qn) = query.quick_notes {
|
||||
defaults.quick_notes_raw.clone_from(qn);
|
||||
}
|
||||
|
||||
let template = AddTemplate {
|
||||
nav_active: "data",
|
||||
is_authenticated,
|
||||
|
|
@ -99,7 +153,7 @@ pub(crate) async fn add_page(
|
|||
brewer_options: brew_form.brewer_options,
|
||||
filter_paper_options: brew_form.filter_paper_options,
|
||||
cafe_options,
|
||||
defaults: brew_form.defaults,
|
||||
defaults,
|
||||
quick_note_options: brew_form.quick_note_options,
|
||||
pre_select_bag_id: query.bag_id,
|
||||
};
|
||||
|
|
|
|||
|
|
@ -1,3 +1,5 @@
|
|||
use std::fmt::Write;
|
||||
|
||||
use crate::domain::brews::{BrewWithDetails, QuickNote, format_brew_time};
|
||||
|
||||
use super::relative_date;
|
||||
|
|
@ -126,6 +128,30 @@ impl BrewView {
|
|||
brew_time_raw: brew.brew.brew_time,
|
||||
}
|
||||
}
|
||||
|
||||
/// Build a URL to the add-brew form pre-filled with this brew's parameters.
|
||||
pub fn brew_again_url(&self) -> String {
|
||||
let mut url = format!(
|
||||
"/add?type=brew&bag_id={}&coffee_weight={}&grinder_id={}&grind_setting={}&brewer_id={}&water_volume={}&water_temp={}",
|
||||
self.bag_id,
|
||||
self.coffee_weight_raw,
|
||||
self.grinder_id,
|
||||
self.grind_setting_raw,
|
||||
self.brewer_id,
|
||||
self.water_volume_raw,
|
||||
self.water_temp_raw,
|
||||
);
|
||||
if let Some(fp_id) = self.filter_paper_id {
|
||||
let _ = write!(url, "&filter_paper_id={fp_id}");
|
||||
}
|
||||
if let Some(bt) = self.brew_time_raw {
|
||||
let _ = write!(url, "&brew_time={bt}");
|
||||
}
|
||||
if !self.quick_notes_raw.is_empty() {
|
||||
let _ = write!(url, "&quick_notes={}", self.quick_notes_raw);
|
||||
}
|
||||
url
|
||||
}
|
||||
}
|
||||
|
||||
pub struct BrewDefaultsView {
|
||||
|
|
@ -141,6 +167,8 @@ pub struct BrewDefaultsView {
|
|||
pub water_volume: i32,
|
||||
pub water_temp: f64,
|
||||
pub brew_time: Option<i32>,
|
||||
/// Comma-separated quick note form values (e.g. "good,too-fast") for pre-filling toggles.
|
||||
pub quick_notes_raw: String,
|
||||
}
|
||||
|
||||
impl Default for BrewDefaultsView {
|
||||
|
|
@ -158,6 +186,7 @@ impl Default for BrewDefaultsView {
|
|||
water_volume: 250,
|
||||
water_temp: 91.0,
|
||||
brew_time: Some(120),
|
||||
quick_notes_raw: String::new(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -181,6 +210,7 @@ impl From<BrewWithDetails> for BrewDefaultsView {
|
|||
water_volume: brew.brew.water_volume,
|
||||
water_temp: brew.brew.water_temp,
|
||||
brew_time: brew.brew.brew_time,
|
||||
quick_notes_raw: String::new(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -89,12 +89,12 @@
|
|||
data-signals:_user-lat="0"
|
||||
data-signals:_user-lng="0"
|
||||
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"
|
||||
data-signals:_qn-good="{% if defaults.quick_notes_raw.contains("good") %}true{% else %}false{% endif %}"
|
||||
data-signals:_qn-too-fast="{% if defaults.quick_notes_raw.contains("too-fast") %}true{% else %}false{% endif %}"
|
||||
data-signals:_qn-too-slow="{% if defaults.quick_notes_raw.contains("too-slow") %}true{% else %}false{% endif %}"
|
||||
data-signals:_qn-too-hot="{% if defaults.quick_notes_raw.contains("too-hot") %}true{% else %}false{% endif %}"
|
||||
data-signals:_qn-under-extracted="{% if defaults.quick_notes_raw.contains("under-extracted") %}true{% else %}false{% endif %}"
|
||||
data-signals:_qn-over-extracted="{% if defaults.quick_notes_raw.contains("over-extracted") %}true{% else %}false{% endif %}"
|
||||
>
|
||||
<h2 class="text-lg font-semibold text-text mb-3">Manual Add</h2>
|
||||
|
||||
|
|
|
|||
|
|
@ -2,33 +2,13 @@
|
|||
{% macro card(brew, is_authenticated) %}
|
||||
<div class="relative w-[45vw] md:w-[200px] h-[280px] shrink-0 snap-start rounded-lg border bg-surface p-4 flex flex-col">
|
||||
{% if is_authenticated %}
|
||||
<form
|
||||
class="absolute top-4 right-3"
|
||||
data-on:submit="$_brewing = true; @post('/api/v1/brews', {contentType: 'form'})"
|
||||
data-on:datastar-fetch="if (!$_brewing) return; if (evt.detail.type === 'finished') { window.location.reload() } else if (evt.detail.type === 'error') { $_brewing = false }"
|
||||
>
|
||||
<input type="hidden" name="bag_id" value="{{ brew.bag_id }}" />
|
||||
<input type="hidden" name="coffee_weight" value="{{ brew.coffee_weight_raw }}" />
|
||||
<input type="hidden" name="grinder_id" value="{{ brew.grinder_id }}" />
|
||||
<input type="hidden" name="grind_setting" value="{{ brew.grind_setting_raw }}" />
|
||||
<input type="hidden" name="brewer_id" value="{{ brew.brewer_id }}" />
|
||||
{% if let Some(fp_id) = brew.filter_paper_id %}
|
||||
<input type="hidden" name="filter_paper_id" value="{{ fp_id }}" />
|
||||
{% endif %}
|
||||
<input type="hidden" name="water_volume" value="{{ brew.water_volume_raw }}" />
|
||||
<input type="hidden" name="water_temp" value="{{ brew.water_temp_raw }}" />
|
||||
<input type="hidden" name="quick_notes" value="{{ brew.quick_notes_raw }}" />
|
||||
{% if let Some(bt) = brew.brew_time_raw %}
|
||||
<input type="hidden" name="brew_time" value="{{ bt }}" />
|
||||
{% endif %}
|
||||
<button
|
||||
type="submit"
|
||||
class="inline-flex h-6 w-6 items-center justify-center rounded-md text-text-muted transition hover:text-accent hover:bg-surface-alt"
|
||||
<a
|
||||
href="{{ brew.brew_again_url() }}"
|
||||
class="absolute top-4 right-3 inline-flex h-6 w-6 items-center justify-center rounded-md text-text-muted transition hover:text-accent hover:bg-surface-alt"
|
||||
title="Brew Again"
|
||||
>
|
||||
{{ icons::refresh("h-4.5 w-4.5") }}
|
||||
</button>
|
||||
</form>
|
||||
</a>
|
||||
{% endif %}
|
||||
<div class="h-[4.5rem] overflow-hidden pr-5">
|
||||
<span class="block font-semibold text-text truncate">{{ brew.roast_name }}</span>
|
||||
|
|
|
|||
|
|
@ -3,26 +3,10 @@
|
|||
|
||||
{% macro brew_actions(brew, navigator) %}
|
||||
<div class="flex items-center gap-4">
|
||||
<form class="inline" data-on:submit="@post('/api/v1/brews?{{ navigator.query() }}', {contentType: 'form', responseOverrides: {selector: '#brew-list', mode: 'replace'}})">
|
||||
<input type="hidden" name="bag_id" value="{{ brew.bag_id }}" />
|
||||
<input type="hidden" name="coffee_weight" value="{{ brew.coffee_weight_raw }}" />
|
||||
<input type="hidden" name="grinder_id" value="{{ brew.grinder_id }}" />
|
||||
<input type="hidden" name="grind_setting" value="{{ brew.grind_setting_raw }}" />
|
||||
<input type="hidden" name="brewer_id" value="{{ brew.brewer_id }}" />
|
||||
{% if let Some(fp_id) = brew.filter_paper_id %}
|
||||
<input type="hidden" name="filter_paper_id" value="{{ fp_id }}" />
|
||||
{% endif %}
|
||||
<input type="hidden" name="water_volume" value="{{ brew.water_volume_raw }}" />
|
||||
<input type="hidden" name="water_temp" value="{{ brew.water_temp_raw }}" />
|
||||
<input type="hidden" name="quick_notes" value="{{ brew.quick_notes_raw }}" />
|
||||
{% if let Some(bt) = brew.brew_time_raw %}
|
||||
<input type="hidden" name="brew_time" value="{{ bt }}" />
|
||||
{% endif %}
|
||||
<button type="submit"
|
||||
<a href="{{ brew.brew_again_url() }}"
|
||||
class="inline-flex items-center gap-1 text-sm font-medium text-accent hover:text-accent-hover">
|
||||
{{ icons::plus_circle("h-4 w-4") }} Brew Again
|
||||
</button>
|
||||
</form>
|
||||
</a>
|
||||
<button type="button"
|
||||
class="inline-flex items-center gap-1 text-sm font-medium text-text-muted hover:text-red-600"
|
||||
data-on:click="confirm('Delete this brew?') && @delete('/api/v1/brews/{{ brew.id }}?{{ navigator.query() }}', {responseOverrides: {selector: '#brew-list', mode: 'replace'}})">
|
||||
|
|
|
|||
Loading…
Reference in a new issue