fix(gui): needs-attention grows to fit with 'show N more' instead of a hidden inner scroll; receipt wording follows the policy
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01MtYEaR6M6Ybdhm6X98BWNu
This commit is contained in:
5 files changed
+83
-42
No files matched your search
@@ -51,9 +51,9 @@ pub fn find(b: &Books) -> Vec<Attention> {
|
|||||||
severity: Severity::Problem,
|
severity: Severity::Problem,
|
||||||
title: format!("{} without receipts", fmt::count(missing.len(), "expense", "expenses")),
|
title: format!("{} without receipts", fmt::count(missing.len(), "expense", "expenses")),
|
||||||
detail: format!(
|
detail: format!(
|
||||||
"{} of spending over the {} threshold has no receipt attached.",
|
"{} of spending has no receipt attached ({}).",
|
||||||
fmt::dollars(total),
|
fmt::dollars(total),
|
||||||
fmt::dollars(b.receipt_threshold)
|
b.receipt_rule()
|
||||||
),
|
),
|
||||||
target: Some(Route::Transactions),
|
target: Some(Route::Transactions),
|
||||||
prompt: format!(
|
prompt: format!(
|
||||||
@@ -67,11 +67,11 @@ pub fn find(b: &Books) -> Vec<Attention> {
|
|||||||
severity: Severity::Problem,
|
severity: Severity::Problem,
|
||||||
title: format!("No receipt: {}", e.description),
|
title: format!("No receipt: {}", e.description),
|
||||||
detail: format!(
|
detail: format!(
|
||||||
"#{} · {} · {} is over the {} receipt threshold.",
|
"#{} · {} · {}: {}.",
|
||||||
e.id,
|
e.id,
|
||||||
fmt::date(e.date),
|
fmt::date(e.date),
|
||||||
fmt::dollars(e.amount),
|
fmt::dollars(e.amount),
|
||||||
fmt::dollars(b.receipt_threshold)
|
b.receipt_rule()
|
||||||
),
|
),
|
||||||
target: Some(Route::Entry(e.id)),
|
target: Some(Route::Entry(e.id)),
|
||||||
prompt: format!(
|
prompt: format!(
|
||||||
|
|||||||
@@ -316,6 +316,19 @@ impl Books {
|
|||||||
Ok(books)
|
Ok(books)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Why an entry needs a receipt, in words: "every entry needs a
|
||||||
|
/// receipt" or "over the $25.00 receipt threshold".
|
||||||
|
pub fn receipt_rule(&self) -> String {
|
||||||
|
if self.receipt_threshold.is_zero() {
|
||||||
|
"every entry needs a receipt".to_string()
|
||||||
|
} else {
|
||||||
|
format!(
|
||||||
|
"over the {} receipt threshold",
|
||||||
|
crate::fmt::dollars(self.receipt_threshold)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
pub fn seq(&self) -> u64 {
|
pub fn seq(&self) -> u64 {
|
||||||
self.journal.len() as u64
|
self.journal.len() as u64
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -243,10 +243,7 @@ pub fn detail(ui: &mut Ui, b: &Books, e: &EntryInfo, shell: &mut Shell<'_>) {
|
|||||||
);
|
);
|
||||||
if all_for_entry.is_empty() {
|
if all_for_entry.is_empty() {
|
||||||
let msg = if e.missing_receipt {
|
let msg = if e.missing_receipt {
|
||||||
format!(
|
format!("None attached, and {}.", b.receipt_rule())
|
||||||
"None attached, and this entry is over the {} receipt threshold.",
|
|
||||||
fmt::dollars(b.receipt_threshold)
|
|
||||||
)
|
|
||||||
} else {
|
} else {
|
||||||
"None attached.".to_string()
|
"None attached.".to_string()
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -283,41 +283,69 @@ fn attention(ui: &mut Ui, b: &Books, shell: &mut Shell<'_>) {
|
|||||||
);
|
);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
egui::ScrollArea::vertical()
|
// No inner scrolling: the card grows to fit. Past a handful of
|
||||||
.id_salt("attention")
|
// items, the rest sit behind a button that says how many.
|
||||||
.max_height(260.0)
|
const FIRST: usize = 6;
|
||||||
.show(ui, |ui| {
|
let id = ui.id().with("attention-expanded");
|
||||||
for (i, a) in b.attention.iter().enumerate() {
|
let mut expanded: bool = ui.ctx().data(|d| d.get_temp(id)).unwrap_or(false);
|
||||||
if i > 0 {
|
let shown = if expanded {
|
||||||
ui.add_space(4.0);
|
b.attention.len()
|
||||||
widgets::rule(ui);
|
} else {
|
||||||
ui.add_space(4.0);
|
b.attention.len().min(FIRST)
|
||||||
|
};
|
||||||
|
for (i, a) in b.attention.iter().take(shown).enumerate() {
|
||||||
|
if i > 0 {
|
||||||
|
ui.add_space(4.0);
|
||||||
|
widgets::rule(ui);
|
||||||
|
ui.add_space(4.0);
|
||||||
|
}
|
||||||
|
let (glyph, color) = match a.severity {
|
||||||
|
Severity::Problem => (icon::WARNING_CIRCLE, p.negative),
|
||||||
|
Severity::Todo => (icon::CIRCLE_DASHED, p.warning),
|
||||||
|
Severity::Info => (icon::INFO, p.muted),
|
||||||
|
};
|
||||||
|
ui.horizontal_top(|ui| {
|
||||||
|
ui.label(RichText::new(glyph).size(16.0).color(color));
|
||||||
|
ui.vertical(|ui| {
|
||||||
|
let t = RichText::new(&a.title).font(theme::semibold(13.0));
|
||||||
|
let r = widgets::link(ui, t);
|
||||||
|
if r.clicked()
|
||||||
|
&& let Some(to) = &a.target
|
||||||
|
{
|
||||||
|
shell.go(to.clone());
|
||||||
}
|
}
|
||||||
let (glyph, color) = match a.severity {
|
ui.label(
|
||||||
Severity::Problem => (icon::WARNING_CIRCLE, p.negative),
|
RichText::new(&a.detail)
|
||||||
Severity::Todo => (icon::CIRCLE_DASHED, p.warning),
|
.font(theme::regular(12.0))
|
||||||
Severity::Info => (icon::INFO, p.muted),
|
.color(p.muted),
|
||||||
};
|
);
|
||||||
ui.horizontal_top(|ui| {
|
widgets::copy_prompt(ui, &a.prompt);
|
||||||
ui.label(RichText::new(glyph).size(16.0).color(color));
|
});
|
||||||
ui.vertical(|ui| {
|
|
||||||
let t = RichText::new(&a.title).font(theme::semibold(13.0));
|
|
||||||
let r = widgets::link(ui, t);
|
|
||||||
if r.clicked()
|
|
||||||
&& let Some(to) = &a.target
|
|
||||||
{
|
|
||||||
shell.go(to.clone());
|
|
||||||
}
|
|
||||||
ui.label(
|
|
||||||
RichText::new(&a.detail)
|
|
||||||
.font(theme::regular(12.0))
|
|
||||||
.color(p.muted),
|
|
||||||
);
|
|
||||||
widgets::copy_prompt(ui, &a.prompt);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
}
|
|
||||||
});
|
});
|
||||||
|
}
|
||||||
|
let hidden = b.attention.len() - shown;
|
||||||
|
if hidden > 0 || expanded && b.attention.len() > FIRST {
|
||||||
|
ui.add_space(8.0);
|
||||||
|
let label = if hidden > 0 {
|
||||||
|
format!("{} Show {} more", icon::CARET_DOWN, hidden)
|
||||||
|
} else {
|
||||||
|
format!("{} Show fewer", icon::CARET_UP)
|
||||||
|
};
|
||||||
|
if ui
|
||||||
|
.add(
|
||||||
|
egui::Button::new(
|
||||||
|
RichText::new(label)
|
||||||
|
.font(theme::semibold(12.5))
|
||||||
|
.color(p.accent),
|
||||||
|
)
|
||||||
|
.frame(false),
|
||||||
|
)
|
||||||
|
.clicked()
|
||||||
|
{
|
||||||
|
expanded = !expanded;
|
||||||
|
ui.ctx().data_mut(|d| d.insert_temp(id, expanded));
|
||||||
|
}
|
||||||
|
}
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -77,7 +77,10 @@ fn screenshots() {
|
|||||||
let books = data::load_once(&source).unwrap();
|
let books = data::load_once(&source).unwrap();
|
||||||
let mut harness = Harness::builder()
|
let mut harness = Harness::builder()
|
||||||
.with_size(egui::vec2(
|
.with_size(egui::vec2(
|
||||||
1440.0,
|
std::env::var("SHOT_WIDTH")
|
||||||
|
.ok()
|
||||||
|
.and_then(|w| w.parse().ok())
|
||||||
|
.unwrap_or(1440.0),
|
||||||
std::env::var("SHOT_HEIGHT")
|
std::env::var("SHOT_HEIGHT")
|
||||||
.ok()
|
.ok()
|
||||||
.and_then(|h| h.parse().ok())
|
.and_then(|h| h.parse().ok())
|
||||||
|
|||||||
Reference in new issue
Block a user