Add a total spendings section
This commit is contained in:
parent
06efebc17b
commit
e9ff7f7dcc
2 changed files with 158 additions and 90 deletions
247
src/compare.rs
247
src/compare.rs
|
|
@ -13,6 +13,7 @@ use itertools::Itertools;
|
|||
|
||||
use crate::{
|
||||
CompareLoad, CompareSide, Report, ReportDate, Spendings, TEXT_EMPH2, TEXT_H1, TEXT_H2,
|
||||
TEXT_NORMAL,
|
||||
};
|
||||
|
||||
pub(crate) struct Compare<'a, F> {
|
||||
|
|
@ -38,7 +39,7 @@ pub(crate) enum CompareMsg {
|
|||
pub(crate) enum Section {
|
||||
Recurring,
|
||||
Variable,
|
||||
Total,
|
||||
Accounts,
|
||||
}
|
||||
|
||||
impl Section {
|
||||
|
|
@ -46,7 +47,7 @@ impl Section {
|
|||
match self {
|
||||
Section::Recurring => "Recurring",
|
||||
Section::Variable => "Variable",
|
||||
Section::Total => "Total",
|
||||
Section::Accounts => "Accounts",
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -116,54 +117,131 @@ where
|
|||
];
|
||||
|
||||
let headings = [
|
||||
Some(
|
||||
column![text("Item").size(TEXT_H1), text("").size(TEXT_H2)]
|
||||
.align_items(Alignment::Center)
|
||||
.into(),
|
||||
),
|
||||
Some(
|
||||
column![text("Left").size(TEXT_H1), heading_left]
|
||||
.align_items(Alignment::Center)
|
||||
.into(),
|
||||
),
|
||||
Some(
|
||||
column![text("Difference").size(TEXT_H1), text("").size(TEXT_H2)]
|
||||
.align_items(Alignment::Center)
|
||||
.into(),
|
||||
),
|
||||
Some(
|
||||
column![text("Right").size(TEXT_H1), heading_right]
|
||||
.align_items(Alignment::Center)
|
||||
.into(),
|
||||
),
|
||||
column![text("Item").size(TEXT_H1), text("").size(TEXT_H2)]
|
||||
.align_items(Alignment::Center)
|
||||
.into(),
|
||||
column![text("Left").size(TEXT_H1), heading_left]
|
||||
.align_items(Alignment::Center)
|
||||
.into(),
|
||||
column![text("Difference").size(TEXT_H1), text("").size(TEXT_H2)]
|
||||
.align_items(Alignment::Center)
|
||||
.into(),
|
||||
column![text("Right").size(TEXT_H1), heading_right]
|
||||
.align_items(Alignment::Center)
|
||||
.into(),
|
||||
];
|
||||
|
||||
fn text_row<'a, T, M>(size: u16, r: [Option<T>; 4]) -> [iced::Element<'a, M>; 4]
|
||||
where
|
||||
T: Into<iced::Element<'a, M>>,
|
||||
{
|
||||
let [a, b, c, d] = r;
|
||||
let map = |e: Option<T>| {
|
||||
e.map(Into::into)
|
||||
.unwrap_or_else(|| text("").size(size).into())
|
||||
};
|
||||
[map(a), map(b), map(c), map(d)]
|
||||
}
|
||||
|
||||
let mk_section = |section: Section| {
|
||||
let status = state.collapsed[section];
|
||||
[
|
||||
Some(
|
||||
row![
|
||||
text(section.name()).size(TEXT_EMPH2),
|
||||
horizontal_space(Length::Fill),
|
||||
toggler(None, !status, move |b| CompareMsg::SetCollapse(section, !b)),
|
||||
]
|
||||
.align_items(Alignment::Center)
|
||||
.into(),
|
||||
),
|
||||
Some(text("").size(TEXT_EMPH2).into()),
|
||||
Some(text("").size(TEXT_EMPH2).into()),
|
||||
Some(text("").size(TEXT_EMPH2).into()),
|
||||
]
|
||||
text_row(
|
||||
TEXT_EMPH2,
|
||||
[
|
||||
Some(
|
||||
row![
|
||||
text(section.name()).size(TEXT_EMPH2),
|
||||
horizontal_space(Length::Fill),
|
||||
toggler(None, !status, move |b| CompareMsg::SetCollapse(section, !b))
|
||||
.width(Length::Shrink),
|
||||
]
|
||||
.align_items(Alignment::Center),
|
||||
),
|
||||
None,
|
||||
None,
|
||||
None,
|
||||
],
|
||||
)
|
||||
};
|
||||
|
||||
let left = self.left.as_ref().map(|(_, r)| &**r);
|
||||
let right = self.right.as_ref().map(|(_, r)| &**r);
|
||||
|
||||
fn compare_row<'b, M>(
|
||||
size: u16,
|
||||
always: Option<&str>,
|
||||
left: Option<(&str, f64)>,
|
||||
right: Option<(&str, f64)>,
|
||||
) -> Vec<[iced::Element<'b, M>; 4]> {
|
||||
let float_text = |f: f64| Some(text(format!("{f:.2}")).size(size));
|
||||
|
||||
match (left, right) {
|
||||
(None, None) => match always {
|
||||
None => vec![],
|
||||
Some(l) => vec![text_row(size, [Some(text(l).size(size)), None, None, None])],
|
||||
},
|
||||
(None, Some((k, v))) => {
|
||||
vec![text_row(
|
||||
size,
|
||||
[Some(text(k).size(size)), None, None, float_text(v)],
|
||||
)]
|
||||
}
|
||||
(Some((k, v)), None) => {
|
||||
vec![text_row(
|
||||
size,
|
||||
[Some(text(k).size(size)), float_text(v), None, None],
|
||||
)]
|
||||
}
|
||||
(Some((lk, lv)), Some((rk, rv))) if lk != rk => {
|
||||
vec![
|
||||
text_row(
|
||||
size,
|
||||
[Some(text(lk).size(size)), float_text(lv), None, None],
|
||||
),
|
||||
text_row(
|
||||
size,
|
||||
[Some(text(rk).size(size)), None, None, float_text(rv)],
|
||||
),
|
||||
]
|
||||
}
|
||||
(Some((k, lv)), Some((_, rv))) => {
|
||||
if rv == 0. || lv == 0. {
|
||||
vec![text_row(
|
||||
size,
|
||||
[
|
||||
Some(text(k).size(size)),
|
||||
float_text(lv),
|
||||
None,
|
||||
float_text(rv),
|
||||
],
|
||||
)]
|
||||
} else {
|
||||
let difference = ((rv / lv - 1.) * 100.) as i32;
|
||||
let sign = match difference.cmp(&0) {
|
||||
Ordering::Less => "- ",
|
||||
Ordering::Equal => "",
|
||||
Ordering::Greater => "+ ",
|
||||
};
|
||||
|
||||
vec![text_row(
|
||||
size,
|
||||
[
|
||||
Some(text(k).size(size)),
|
||||
float_text(lv),
|
||||
Some(text(format!("{sign}{}%", difference.abs())).size(size)),
|
||||
float_text(rv),
|
||||
],
|
||||
)]
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn item_compare<'a, I, M>(
|
||||
collapse: bool,
|
||||
left: Option<I>,
|
||||
right: Option<I>,
|
||||
) -> Vec<[Option<iced::Element<'a, M>>; 4]>
|
||||
) -> Vec<[iced::Element<'a, M>; 4]>
|
||||
where
|
||||
I: IntoIterator<Item = (&'a str, f64)>,
|
||||
{
|
||||
|
|
@ -173,7 +251,7 @@ where
|
|||
|
||||
let to_btree = |i: I| i.into_iter().collect::<BTreeMap<_, _>>();
|
||||
|
||||
let float_text = |f: f64| Some(text(format!("{f:.2}")).into());
|
||||
let float_text = |f: f64| Some(text(format!("{f:.2}")).size(TEXT_NORMAL));
|
||||
|
||||
let (left, right) = match (left, right) {
|
||||
(None, None) => return Vec::new(),
|
||||
|
|
@ -182,14 +260,24 @@ where
|
|||
return u
|
||||
.into_iter()
|
||||
.sorted_by_key(|(k, _)| *k)
|
||||
.map(|(k, v)| [Some(text(k).into()), float_text(v), None, None])
|
||||
.map(|(k, v)| {
|
||||
text_row(
|
||||
TEXT_NORMAL,
|
||||
[Some(text(k).size(TEXT_NORMAL)), float_text(v), None, None],
|
||||
)
|
||||
})
|
||||
.collect();
|
||||
}
|
||||
(None, Some(u)) => {
|
||||
return u
|
||||
.into_iter()
|
||||
.sorted_by_key(|(k, _)| *k)
|
||||
.map(|(k, v)| [Some(text(k).into()), None, None, float_text(v)])
|
||||
.map(|(k, v)| {
|
||||
text_row(
|
||||
TEXT_NORMAL,
|
||||
[Some(text(k).size(TEXT_NORMAL)), None, None, float_text(v)],
|
||||
)
|
||||
})
|
||||
.collect();
|
||||
}
|
||||
};
|
||||
|
|
@ -221,49 +309,21 @@ where
|
|||
let mut compare = Vec::new();
|
||||
|
||||
type Item<'a> = Option<(&'a &'a str, &'a f64)>;
|
||||
let mut insert_row = |l: Item, r: Item| match (l, r) {
|
||||
(None, None) => false,
|
||||
(None, Some((k, v))) => {
|
||||
compare.push([Some(text(k).into()), None, None, float_text(*v)]);
|
||||
true
|
||||
}
|
||||
(Some((k, v)), None) => {
|
||||
compare.push([Some(text(k).into()), float_text(*v), None, None]);
|
||||
true
|
||||
}
|
||||
(Some((lk, lv)), Some((rk, rv))) if lk != rk => {
|
||||
compare.push([Some(text(lk).into()), float_text(*lv), None, None]);
|
||||
|
||||
compare.push([Some(text(rk).into()), None, None, float_text(*rv)]);
|
||||
|
||||
true
|
||||
}
|
||||
(Some((k, lv)), Some((_, rv))) => {
|
||||
if *rv == 0. || *lv == 0. {
|
||||
compare.push([
|
||||
Some(text(k).into()),
|
||||
float_text(*lv),
|
||||
None,
|
||||
float_text(*rv),
|
||||
]);
|
||||
} else {
|
||||
let difference = ((rv / lv - 1.) * 100.) as i32;
|
||||
let sign = match difference.cmp(&0) {
|
||||
Ordering::Less => "- ",
|
||||
Ordering::Equal => "",
|
||||
Ordering::Greater => "+ ",
|
||||
};
|
||||
|
||||
compare.push([
|
||||
Some(text(k).into()),
|
||||
float_text(*lv),
|
||||
Some(text(format!("{sign}{}%", difference.abs())).into()),
|
||||
float_text(*rv),
|
||||
]);
|
||||
}
|
||||
|
||||
true
|
||||
}
|
||||
let mut insert_row = |l: Item, r: Item| {
|
||||
let mut inserted = false;
|
||||
compare.extend(
|
||||
compare_row(
|
||||
TEXT_NORMAL,
|
||||
None,
|
||||
l.map(|(a, b)| (*a, *b)),
|
||||
r.map(|(a, b)| (*a, *b)),
|
||||
)
|
||||
.into_iter()
|
||||
.inspect(|_| {
|
||||
inserted = true;
|
||||
}),
|
||||
);
|
||||
inserted
|
||||
};
|
||||
|
||||
loop {
|
||||
|
|
@ -288,7 +348,7 @@ where
|
|||
compare
|
||||
}
|
||||
|
||||
fn mk_total<'a>(
|
||||
fn mk_accounts<'a>(
|
||||
archive: &BTreeMap<ReportDate, Report>,
|
||||
side: Option<&'a dyn Spendings>,
|
||||
) -> Option<impl Iterator<Item = (&'a str, f64)>> {
|
||||
|
|
@ -298,6 +358,12 @@ where
|
|||
Some(std::iter::once(("Main", main_account)).chain(side.savings()))
|
||||
}
|
||||
|
||||
let mk_total = |side: Option<&dyn Spendings>| {
|
||||
let side = side?;
|
||||
|
||||
Some(("Total", side.total_spendings()))
|
||||
};
|
||||
|
||||
scrollable(table::<_, _, iced::Element<_>, _>(
|
||||
properties,
|
||||
itertools::chain![
|
||||
|
|
@ -314,11 +380,12 @@ where
|
|||
left.map(|r| r.variable()),
|
||||
right.map(|r| r.variable())
|
||||
),
|
||||
iter::once(mk_section(Section::Total)),
|
||||
compare_row(TEXT_EMPH2, Some("Total"), mk_total(left), mk_total(right)),
|
||||
iter::once(mk_section(Section::Accounts)),
|
||||
item_compare(
|
||||
state.collapsed[Section::Variable],
|
||||
mk_total(self.archive, left),
|
||||
mk_total(self.archive, right),
|
||||
state.collapsed[Section::Accounts],
|
||||
mk_accounts(self.archive, left),
|
||||
mk_accounts(self.archive, right),
|
||||
),
|
||||
],
|
||||
))
|
||||
|
|
@ -336,7 +403,7 @@ pub struct ColumnProperties {
|
|||
pub fn table<'a, I, J, T, M>(props: Vec<ColumnProperties>, items: I) -> Row<'a, M>
|
||||
where
|
||||
I: IntoIterator<Item = J>,
|
||||
J: IntoIterator<Item = Option<T>>,
|
||||
J: IntoIterator<Item = T>,
|
||||
T: Into<iced::Element<'a, M>>,
|
||||
M: 'a,
|
||||
{
|
||||
|
|
@ -350,7 +417,7 @@ where
|
|||
col.push(horizontal_rule(5.).into());
|
||||
}
|
||||
|
||||
let inner = item.map(Into::into).unwrap_or_else(|| text("").into());
|
||||
let inner = item.into();
|
||||
col.push(inner);
|
||||
});
|
||||
columns
|
||||
|
|
|
|||
|
|
@ -29,6 +29,7 @@ mod edit;
|
|||
|
||||
const TEXT_H1: u16 = 30;
|
||||
const TEXT_H2: u16 = 25;
|
||||
const TEXT_NORMAL: u16 = 15;
|
||||
const TEXT_EMPH1: u16 = 17;
|
||||
const TEXT_EMPH2: u16 = 20;
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue