From ae31c2793f7540bb7e2cb6f6f828c4319366938a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Michael=20Loipf=C3=BChrer?= Date: Thu, 12 Jan 2023 22:59:54 +0100 Subject: [PATCH] fix(web): correct balance graph handling --- .../accounts/BalanceHistoryGraph.tsx | 101 +++++++++++------- frontend/libs/core/src/lib/accounts.ts | 7 +- .../redux/src/lib/accounts/accountSlice.ts | 14 ++- 3 files changed, 73 insertions(+), 49 deletions(-) diff --git a/frontend/apps/web/src/components/accounts/BalanceHistoryGraph.tsx b/frontend/apps/web/src/components/accounts/BalanceHistoryGraph.tsx index a4664835..91e3903e 100644 --- a/frontend/apps/web/src/components/accounts/BalanceHistoryGraph.tsx +++ b/frontend/apps/web/src/components/accounts/BalanceHistoryGraph.tsx @@ -6,7 +6,7 @@ import { selectTransactionByIdMap, } from "@abrechnung/redux"; import { fromISOString, toISODateString } from "@abrechnung/utils"; -import { Box, Divider, Theme, Typography, useTheme } from "@mui/material"; +import { Card, Box, Divider, Theme, Typography, useTheme } from "@mui/material"; import { PointMouseHandler, PointTooltipProps, ResponsiveLine, Serie } from "@nivo/line"; import { DateTime } from "luxon"; import React from "react"; @@ -22,7 +22,6 @@ interface Props { export const BalanceHistoryGraph: React.FC = ({ groupId, accountId }) => { const theme: Theme = useTheme(); - const balanceHistory = useAppSelector((state) => selectAccountBalanceHistory({ state, groupId, accountId })); const navigate = useNavigate(); const currencySymbol = useAppSelector((state) => @@ -34,36 +33,64 @@ export const BalanceHistoryGraph: React.FC = ({ groupId, accountId }) => const accountNameMap = useAppSelector((state) => selectAccountIdToNameMap({ state: selectAccountSlice(state), groupId }) ); - - const graphData: Serie[] = [ - { - id: "positive", - data: balanceHistory.map((entry, index, arr) => { - const prevEntry = index > 0 ? arr[index - 1] : null; - const nextEntry = index < arr.length - 1 ? arr[index + 1] : null; + const { graphData, seriesColors, areaBaselineValue } = useAppSelector((state) => { + const balanceHistory = selectAccountBalanceHistory({ state, groupId, accountId }); + const { hasNegativeEntries, hasPositiveEntries, max, min } = balanceHistory.reduce( + (acc, curr) => { + const neg = curr.balance < 0; + const pos = curr.balance >= 0; return { - x: fromISOString(entry.date), - y: - entry.balance >= 0 || - (prevEntry && prevEntry.balance >= 0) || - (nextEntry && nextEntry.balance >= 0) - ? entry.balance - : null, - changeOrigin: entry.changeOrigin, + hasNegativeEntries: acc.hasNegativeEntries || neg, + hasPositiveEntries: acc.hasPositiveEntries || pos, + max: Math.max(curr.balance, acc.max), + min: Math.min(curr.balance, acc.min), }; - }), - }, - { - id: "negative", - data: balanceHistory.map((entry) => { - return { + }, + { hasNegativeEntries: false, hasPositiveEntries: false, max: -Infinity, min: Infinity } + ); + + const areaBaselineValue = + balanceHistory.length === 0 ? undefined : !hasNegativeEntries ? min : !hasPositiveEntries ? max : undefined; + + const graphData: Serie[] = []; + let lastPoint = balanceHistory[0]; + const makeSerie = (): Serie => { + return { + id: `serie-${graphData.length}`, + data: [], + }; + }; + let currentSeries = makeSerie(); + for (const entry of balanceHistory) { + if (lastPoint === undefined) { + break; + } + const hasDifferentSign = Math.sign(lastPoint.balance) !== Math.sign(entry.balance); + currentSeries.data.push({ + x: fromISOString(entry.date), + y: entry.balance, + changeOrigin: entry.changeOrigin, + }); + if (hasDifferentSign) { + graphData.push(currentSeries); + currentSeries = makeSerie(); + currentSeries.data.push({ x: fromISOString(entry.date), - y: entry.balance < 0 ? entry.balance : null, + y: entry.balance, changeOrigin: entry.changeOrigin, - }; - }), - }, - ]; + }); + } + lastPoint = entry; + } + graphData.push(currentSeries); + const seriesColors: string[] = graphData.map((serie) => + serie.data[0].y >= 0 ? theme.palette.success.main : theme.palette.error.main + ); + + console.log(graphData); + + return { graphData, seriesColors, areaBaselineValue }; + }); const onClick: PointMouseHandler = (point, event) => { const changeOrigin: BalanceChangeOrigin = (point.data as any).changeOrigin; @@ -87,16 +114,7 @@ export const BalanceHistoryGraph: React.FC = ({ groupId, accountId }) => ); return ( - +
{DateTime.fromJSDate(point.data.x as Date).toISODate()} @@ -121,23 +139,24 @@ export const BalanceHistoryGraph: React.FC = ({ groupId, accountId }) => {icon} {transactionMap[changeOrigin.id].name} )} - + ); }; return (
`${toISODateString(p.x as Date)}: ${p.y}`} useMesh={true} - yFormat=">-.2f" axisLeft={{ format: (value: number) => `${value.toFixed(2)} ${currencySymbol}`, }} diff --git a/frontend/libs/core/src/lib/accounts.ts b/frontend/libs/core/src/lib/accounts.ts index 10142e99..c63d8ff9 100644 --- a/frontend/libs/core/src/lib/accounts.ts +++ b/frontend/libs/core/src/lib/accounts.ts @@ -2,6 +2,7 @@ import { Account, AccountBalanceMap, Transaction, + ClearingAccount, TransactionPosition, ClearingShares, TransactionBalanceEffect, @@ -183,7 +184,7 @@ export interface BalanceHistoryEntry { export const computeAccountBalanceHistory = ( accountId: number, - clearingAccounts: Account[], + clearingAccounts: ClearingAccount[], balances: AccountBalanceMap, transactions: Transaction[], // sorted after last change date transactionBalanceEffects: { [k: number]: TransactionBalanceEffect } @@ -198,7 +199,7 @@ export const computeAccountBalanceHistory = ( const a = balanceEffect[accountId]; if (a) { balanceChanges.push({ - date: transaction.lastChanged, + date: transaction.billedAt, change: a.total, changeOrigin: { type: "transaction", @@ -211,7 +212,7 @@ export const computeAccountBalanceHistory = ( for (const account of clearingAccounts) { if (balances[account.id]?.clearingResolution[accountId] !== undefined) { balanceChanges.push({ - date: account.lastChanged, + date: account.dateInfo, change: balances[account.id].clearingResolution[accountId], changeOrigin: { type: "clearing", diff --git a/frontend/libs/redux/src/lib/accounts/accountSlice.ts b/frontend/libs/redux/src/lib/accounts/accountSlice.ts index 51211296..31dc2a42 100644 --- a/frontend/libs/redux/src/lib/accounts/accountSlice.ts +++ b/frontend/libs/redux/src/lib/accounts/accountSlice.ts @@ -1,5 +1,5 @@ import { Api } from "@abrechnung/api"; -import { Account, AccountBase, AccountType } from "@abrechnung/types"; +import { Account, AccountBase, AccountType, PersonalAccount, ClearingAccount } from "@abrechnung/types"; import { createAsyncThunk, createSlice, PayloadAction, Draft } from "@reduxjs/toolkit"; import { AccountSliceState, AccountState, ENABLE_OFFLINE_MODE, IRootState, StateStatus } from "../types"; import { getGroupScopedState } from "../utils"; @@ -108,14 +108,18 @@ export const selectAccountsFilteredCount = memoize( } ); -export const selectGroupAccountsFilteredInternal = (args: { +type NarrowedAccount = AccountTypeT extends "personal" + ? PersonalAccount + : ClearingAccount; + +export const selectGroupAccountsFilteredInternal = (args: { state: AccountSliceState; groupId: number; - type: AccountType; -}): Account[] => { + type: AccountTypeT; +}): NarrowedAccount[] => { const { state, groupId, type } = args; const accounts = selectGroupAccountsInternal({ state, groupId }); - return accounts.filter((acc: Account) => acc.type === type); + return accounts.filter((acc: Account) => acc.type === type) as NarrowedAccount[]; }; export const selectSortedAccounts = memoize(