Add a total spendings section
This commit is contained in:
parent
06efebc17b
commit
e9ff7f7dcc
2 changed files with 158 additions and 90 deletions
207
src/compare.rs
207
src/compare.rs
|
|
@ -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
|
||||||
|
|
|
||||||
|
|
@ -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;
|
||||||
|
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue