Add a total spendings section

This commit is contained in:
traxys 2023-12-09 17:28:21 +01:00
parent 06efebc17b
commit e9ff7f7dcc
2 changed files with 158 additions and 90 deletions

View file

@ -13,6 +13,7 @@ use itertools::Itertools;
use crate::{ use crate::{
CompareLoad, CompareSide, Report, ReportDate, Spendings, TEXT_EMPH2, TEXT_H1, TEXT_H2, CompareLoad, CompareSide, Report, ReportDate, Spendings, TEXT_EMPH2, TEXT_H1, TEXT_H2,
TEXT_NORMAL,
}; };
pub(crate) struct Compare<'a, F> { pub(crate) struct Compare<'a, F> {
@ -38,7 +39,7 @@ pub(crate) enum CompareMsg {
pub(crate) enum Section { pub(crate) enum Section {
Recurring, Recurring,
Variable, Variable,
Total, Accounts,
} }
impl Section { impl Section {
@ -46,7 +47,7 @@ impl Section {
match self { match self {
Section::Recurring => "Recurring", Section::Recurring => "Recurring",
Section::Variable => "Variable", Section::Variable => "Variable",
Section::Total => "Total", Section::Accounts => "Accounts",
} }
} }
} }
@ -116,54 +117,131 @@ where
]; ];
let headings = [ let headings = [
Some(
column![text("Item").size(TEXT_H1), text("").size(TEXT_H2)] column![text("Item").size(TEXT_H1), text("").size(TEXT_H2)]
.align_items(Alignment::Center) .align_items(Alignment::Center)
.into(), .into(),
),
Some(
column![text("Left").size(TEXT_H1), heading_left] column![text("Left").size(TEXT_H1), heading_left]
.align_items(Alignment::Center) .align_items(Alignment::Center)
.into(), .into(),
),
Some(
column![text("Difference").size(TEXT_H1), text("").size(TEXT_H2)] column![text("Difference").size(TEXT_H1), text("").size(TEXT_H2)]
.align_items(Alignment::Center) .align_items(Alignment::Center)
.into(), .into(),
),
Some(
column![text("Right").size(TEXT_H1), heading_right] column![text("Right").size(TEXT_H1), heading_right]
.align_items(Alignment::Center) .align_items(Alignment::Center)
.into(), .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 mk_section = |section: Section| {
let status = state.collapsed[section]; let status = state.collapsed[section];
text_row(
TEXT_EMPH2,
[ [
Some( Some(
row![ row![
text(section.name()).size(TEXT_EMPH2), text(section.name()).size(TEXT_EMPH2),
horizontal_space(Length::Fill), horizontal_space(Length::Fill),
toggler(None, !status, move |b| CompareMsg::SetCollapse(section, !b)), toggler(None, !status, move |b| CompareMsg::SetCollapse(section, !b))
.width(Length::Shrink),
] ]
.align_items(Alignment::Center) .align_items(Alignment::Center),
.into(),
), ),
Some(text("").size(TEXT_EMPH2).into()), None,
Some(text("").size(TEXT_EMPH2).into()), None,
Some(text("").size(TEXT_EMPH2).into()), None,
] ],
)
}; };
let left = self.left.as_ref().map(|(_, r)| &**r); let left = self.left.as_ref().map(|(_, r)| &**r);
let right = self.right.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>( fn item_compare<'a, I, M>(
collapse: bool, collapse: bool,
left: Option<I>, left: Option<I>,
right: Option<I>, right: Option<I>,
) -> Vec<[Option<iced::Element<'a, M>>; 4]> ) -> Vec<[iced::Element<'a, M>; 4]>
where where
I: IntoIterator<Item = (&'a str, f64)>, I: IntoIterator<Item = (&'a str, f64)>,
{ {
@ -173,7 +251,7 @@ where
let to_btree = |i: I| i.into_iter().collect::<BTreeMap<_, _>>(); 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) { let (left, right) = match (left, right) {
(None, None) => return Vec::new(), (None, None) => return Vec::new(),
@ -182,14 +260,24 @@ where
return u return u
.into_iter() .into_iter()
.sorted_by_key(|(k, _)| *k) .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(); .collect();
} }
(None, Some(u)) => { (None, Some(u)) => {
return u return u
.into_iter() .into_iter()
.sorted_by_key(|(k, _)| *k) .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(); .collect();
} }
}; };
@ -221,49 +309,21 @@ where
let mut compare = Vec::new(); let mut compare = Vec::new();
type Item<'a> = Option<(&'a &'a str, &'a f64)>; type Item<'a> = Option<(&'a &'a str, &'a f64)>;
let mut insert_row = |l: Item, r: Item| match (l, r) { let mut insert_row = |l: Item, r: Item| {
(None, None) => false, let mut inserted = false;
(None, Some((k, v))) => { compare.extend(
compare.push([Some(text(k).into()), None, None, float_text(*v)]); compare_row(
true TEXT_NORMAL,
}
(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, None,
float_text(*rv), l.map(|(a, b)| (*a, *b)),
]); r.map(|(a, b)| (*a, *b)),
} else { )
let difference = ((rv / lv - 1.) * 100.) as i32; .into_iter()
let sign = match difference.cmp(&0) { .inspect(|_| {
Ordering::Less => "- ", inserted = true;
Ordering::Equal => "", }),
Ordering::Greater => "+ ", );
}; inserted
compare.push([
Some(text(k).into()),
float_text(*lv),
Some(text(format!("{sign}{}%", difference.abs())).into()),
float_text(*rv),
]);
}
true
}
}; };
loop { loop {
@ -288,7 +348,7 @@ where
compare compare
} }
fn mk_total<'a>( fn mk_accounts<'a>(
archive: &BTreeMap<ReportDate, Report>, archive: &BTreeMap<ReportDate, Report>,
side: Option<&'a dyn Spendings>, side: Option<&'a dyn Spendings>,
) -> Option<impl Iterator<Item = (&'a str, f64)>> { ) -> Option<impl Iterator<Item = (&'a str, f64)>> {
@ -298,6 +358,12 @@ where
Some(std::iter::once(("Main", main_account)).chain(side.savings())) 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<_>, _>( scrollable(table::<_, _, iced::Element<_>, _>(
properties, properties,
itertools::chain![ itertools::chain![
@ -314,11 +380,12 @@ where
left.map(|r| r.variable()), left.map(|r| r.variable()),
right.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( item_compare(
state.collapsed[Section::Variable], state.collapsed[Section::Accounts],
mk_total(self.archive, left), mk_accounts(self.archive, left),
mk_total(self.archive, right), 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> pub fn table<'a, I, J, T, M>(props: Vec<ColumnProperties>, items: I) -> Row<'a, M>
where where
I: IntoIterator<Item = J>, I: IntoIterator<Item = J>,
J: IntoIterator<Item = Option<T>>, J: IntoIterator<Item = T>,
T: Into<iced::Element<'a, M>>, T: Into<iced::Element<'a, M>>,
M: 'a, M: 'a,
{ {
@ -350,7 +417,7 @@ where
col.push(horizontal_rule(5.).into()); col.push(horizontal_rule(5.).into());
} }
let inner = item.map(Into::into).unwrap_or_else(|| text("").into()); let inner = item.into();
col.push(inner); col.push(inner);
}); });
columns columns

View file

@ -29,6 +29,7 @@ mod edit;
const TEXT_H1: u16 = 30; const TEXT_H1: u16 = 30;
const TEXT_H2: u16 = 25; const TEXT_H2: u16 = 25;
const TEXT_NORMAL: u16 = 15;
const TEXT_EMPH1: u16 = 17; const TEXT_EMPH1: u16 = 17;
const TEXT_EMPH2: u16 = 20; const TEXT_EMPH2: u16 = 20;