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::{
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

View file

@ -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;