diff --git a/packages/desktop-client/src/components/reports/graphs/SpendingGraph.tsx b/packages/desktop-client/src/components/reports/graphs/SpendingGraph.tsx index 1592e0b99bedce59b9cc0199bd01ab2481c2f040..f6ad918a541cb7e280bb3566e275291047a53ceb 100644 --- a/packages/desktop-client/src/components/reports/graphs/SpendingGraph.tsx +++ b/packages/desktop-client/src/components/reports/graphs/SpendingGraph.tsx @@ -47,6 +47,7 @@ type CustomTooltipProps = { thisMonth?: string; lastYear?: string; selection?: string; + compare?: string; }; const CustomTooltip = ({ @@ -56,6 +57,7 @@ const CustomTooltip = ({ thisMonth, lastYear, selection, + compare, }: CustomTooltipProps) => { if (active && payload && payload.length) { const comparison = @@ -86,7 +88,7 @@ const CustomTooltip = ({ <div style={{ lineHeight: 1.5 }}> {payload[0].payload.months[thisMonth].cumulative ? ( <AlignedText - left="This month:" + left={compare === 'thisMonth' ? 'This month:' : 'Last month:'} right={amountToCurrency( payload[0].payload.months[thisMonth].cumulative * -1, )} @@ -96,10 +98,12 @@ const CustomTooltip = ({ <AlignedText left={ selection === 'average' - ? 'Average' + ? 'Average:' : selection === lastYear - ? 'Last year' - : 'Last month' + ? 'Last year:' + : compare === 'thisMonth' + ? 'Last month:' + : '2 months ago:' } right={amountToCurrency(comparison)} /> @@ -125,6 +129,7 @@ type SpendingGraphProps = { data: SpendingEntity; compact?: boolean; mode: string; + compare: string; }; export function SpendingGraph({ @@ -132,12 +137,19 @@ export function SpendingGraph({ data, compact, mode, + compare, }: SpendingGraphProps) { const privacyMode = usePrivacyMode(); const balanceTypeOp = 'cumulative'; - const thisMonth = monthUtils.currentMonth(); - const lastMonth = monthUtils.subMonths(monthUtils.currentMonth(), 1); - const lastYear = monthUtils.prevYear(monthUtils.currentMonth()); + const thisMonth = monthUtils.subMonths( + monthUtils.currentMonth(), + compare === 'thisMonth' ? 0 : 1, + ); + const previousMonth = monthUtils.subMonths( + monthUtils.currentMonth(), + compare === 'thisMonth' ? 1 : 2, + ); + const lastYear = monthUtils.prevYear(thisMonth); let selection; switch (mode) { case 'average': @@ -147,7 +159,7 @@ export function SpendingGraph({ selection = lastYear; break; default: - selection = lastMonth; + selection = previousMonth; break; } @@ -256,6 +268,7 @@ export function SpendingGraph({ thisMonth={thisMonth} lastYear={lastYear} selection={selection} + compare={compare} /> } formatter={numberFormatterTooltip} diff --git a/packages/desktop-client/src/components/reports/reports/Spending.tsx b/packages/desktop-client/src/components/reports/reports/Spending.tsx index 529c62c06fca87d56002049d3b82ddb0c55c36b2..82992ae9336bb86944afa1f77afb418dbd43c4f5 100644 --- a/packages/desktop-client/src/components/reports/reports/Spending.tsx +++ b/packages/desktop-client/src/components/reports/reports/Spending.tsx @@ -14,6 +14,7 @@ import { AlignedText } from '../../common/AlignedText'; import { Block } from '../../common/Block'; import { Button } from '../../common/Button'; import { Paragraph } from '../../common/Paragraph'; +import { Select } from '../../common/Select'; import { Text } from '../../common/Text'; import { Tooltip } from '../../common/Tooltip'; import { View } from '../../common/View'; @@ -45,15 +46,19 @@ export function Spending() { ); const [spendingReportTime = 'lastMonth', setSpendingReportTime] = useLocalPref('spendingReportTime'); + const [spendingReportCompare = 'thisMonth', setSpendingReportCompare] = + useLocalPref('spendingReportCompare'); const [dataCheck, setDataCheck] = useState(false); + const [compare, setCompare] = useState(spendingReportCompare); const [mode, setMode] = useState(spendingReportTime); const parseFilter = spendingReportFilter && JSON.parse(spendingReportFilter); const filterSaved = JSON.stringify(parseFilter.conditions) === JSON.stringify(conditions) && parseFilter.conditionsOp === conditionsOp && - spendingReportTime === mode; + spendingReportTime === mode && + spendingReportCompare === compare; useEffect(() => { const checkFilter = @@ -70,8 +75,9 @@ export function Spending() { conditions, conditionsOp, setDataCheck, + compare, }); - }, [categories, conditions, conditionsOp]); + }, [categories, conditions, conditionsOp, compare]); const data = useReport('default', getGraphData); const navigate = useNavigate(); @@ -89,6 +95,7 @@ export function Spending() { }), ); setSpendingReportTime(mode); + setSpendingReportCompare(compare); }; const showAverage = @@ -99,12 +106,20 @@ export function Spending() { ) > 0; const todayDay = - monthUtils.getDay(monthUtils.currentDay()) - 1 >= 28 + compare === 'lastMonth' ? 27 - : monthUtils.getDay(monthUtils.currentDay()) - 1; + : monthUtils.getDay(monthUtils.currentDay()) - 1 >= 28 + ? 27 + : monthUtils.getDay(monthUtils.currentDay()) - 1; - const showLastYear = Math.abs(data.intervalData[27].lastYear) > 0; - const showLastMonth = Math.abs(data.intervalData[27].lastMonth) > 0; + const showLastYear = + Math.abs( + data.intervalData[27][ + compare === 'thisMonth' ? 'lastYear' : 'lastYearPrevious' + ], + ) > 0; + const showPreviousMonth = + Math.abs(data.intervalData[27][spendingReportTime]) > 0; return ( <Page header={ @@ -231,7 +246,7 @@ export function Spending() { marginBottom: 5, }} > - {showLastMonth && ( + {showPreviousMonth && ( <View style={{ ...styles.mediumText, @@ -240,24 +255,49 @@ export function Spending() { }} > <AlignedText - left={<Block>Spent MTD:</Block>} + left={ + <Block> + Spent{' '} + {compare === 'thisMonth' ? 'MTD' : 'Last Month'}: + </Block> + } right={ <Text> <PrivacyFilter blurIntensity={5}> {amountToCurrency( - Math.abs(data.intervalData[todayDay].thisMonth), + Math.abs( + data.intervalData[todayDay][ + compare === 'thisMonth' + ? 'thisMonth' + : 'lastMonth' + ], + ), )} </PrivacyFilter> </Text> } /> <AlignedText - left={<Block>Spent Last MTD:</Block>} + left={ + <Block> + Spent{' '} + {compare === 'thisMonth' + ? ' Last MTD' + : '2 Months Ago'} + : + </Block> + } right={ <Text> <PrivacyFilter blurIntensity={5}> {amountToCurrency( - Math.abs(data.intervalData[todayDay].lastMonth), + Math.abs( + data.intervalData[todayDay][ + compare === 'thisMonth' + ? 'lastMonth' + : 'twoMonthsPrevious' + ], + ), )} </PrivacyFilter> </Text> @@ -267,7 +307,11 @@ export function Spending() { )} {showAverage && ( <AlignedText - left={<Block>Spent Average MTD:</Block>} + left={ + <Block> + Spent Average{compare === 'thisMonth' && ' MTD'}: + </Block> + } right={ <Text> <PrivacyFilter blurIntensity={5}> @@ -281,7 +325,7 @@ export function Spending() { )} </View> </View> - {!showLastMonth ? ( + {!showPreviousMonth ? ( <View style={{ marginTop: 30 }}> <h1>Additional data required to generate graph</h1> <Paragraph> @@ -298,18 +342,46 @@ export function Spending() { flexDirection: 'row', }} > + <Text + style={{ + paddingRight: 5, + }} + > + Compare + </Text> + <Select + value={compare} + onChange={e => { + setCompare(e); + if (mode === 'lastMonth') setMode('twoMonthsPrevious'); + if (mode === 'twoMonthsPrevious') setMode('lastMonth'); + }} + options={[ + ['thisMonth', 'this month'], + ['lastMonth', 'last month'], + ]} + /> <Text style={{ paddingRight: 10, + paddingLeft: 5, }} > - Compare this month to: + to the: </Text> <ModeButton - selected={mode === 'lastMonth'} - onSelect={() => setMode('lastMonth')} + selected={['lastMonth', 'twoMonthsPrevious'].includes( + mode, + )} + onSelect={() => + setMode( + compare === 'thisMonth' + ? 'lastMonth' + : 'twoMonthsPrevious', + ) + } > - Last month + Month previous </ModeButton> {showLastYear && ( <ModeButton @@ -335,6 +407,7 @@ export function Spending() { compact={false} data={data} mode={mode} + compare={compare} /> ) : ( <LoadingIndicator message="Loading report..." /> diff --git a/packages/desktop-client/src/components/reports/reports/SpendingCard.tsx b/packages/desktop-client/src/components/reports/reports/SpendingCard.tsx index 94deb49fb3920b79e14f5ff3d13c98c8d1d486a6..83a313bcaa036e4b2e42b6510cde73d78c897fae 100644 --- a/packages/desktop-client/src/components/reports/reports/SpendingCard.tsx +++ b/packages/desktop-client/src/components/reports/reports/SpendingCard.tsx @@ -23,6 +23,9 @@ export function SpendingCard() { const [isCardHovered, setIsCardHovered] = useState(false); const [spendingReportFilter = ''] = useLocalPref('spendingReportFilter'); const [spendingReportTime = 'lastMonth'] = useLocalPref('spendingReportTime'); + const [spendingReportCompare = 'thisMonth'] = useLocalPref( + 'spendingReportCompare', + ); const parseFilter = spendingReportFilter && JSON.parse(spendingReportFilter); const getGraphData = useMemo(() => { @@ -30,18 +33,21 @@ export function SpendingCard() { categories, conditions: parseFilter.conditions, conditionsOp: parseFilter.conditionsOp, + compare: spendingReportCompare, }); - }, [categories, parseFilter]); + }, [categories, parseFilter, spendingReportCompare]); const data = useReport('default', getGraphData); const todayDay = - monthUtils.getDay(monthUtils.currentDay()) - 1 >= 28 + spendingReportCompare === 'lastMonth' ? 27 - : monthUtils.getDay(monthUtils.currentDay()) - 1; + : monthUtils.getDay(monthUtils.currentDay()) - 1 >= 28 + ? 27 + : monthUtils.getDay(monthUtils.currentDay()) - 1; const difference = data && data.intervalData[todayDay][spendingReportTime] - - data.intervalData[todayDay].thisMonth; + data.intervalData[todayDay][spendingReportCompare]; const showLastMonth = data && Math.abs(data.intervalData[27].lastMonth) > 0; return ( @@ -99,6 +105,7 @@ export function SpendingCard() { compact={true} data={data} mode={spendingReportTime} + compare={spendingReportCompare} /> ) : ( <LoadingIndicator message="Loading report..." /> diff --git a/packages/desktop-client/src/components/reports/spreadsheets/spending-spreadsheet.ts b/packages/desktop-client/src/components/reports/spreadsheets/spending-spreadsheet.ts index 205a1b160b3a1f1bf157a4c9478a83a8a47ffa80..718203c73aa23533f647f910801bc253e86d61bb 100644 --- a/packages/desktop-client/src/components/reports/spreadsheets/spending-spreadsheet.ts +++ b/packages/desktop-client/src/components/reports/spreadsheets/spending-spreadsheet.ts @@ -25,6 +25,7 @@ type createSpendingSpreadsheetProps = { conditions?: RuleConditionEntity[]; conditionsOp?: string; setDataCheck?: (value: boolean) => void; + compare?: string; }; export function createSpendingSpreadsheet({ @@ -32,11 +33,20 @@ export function createSpendingSpreadsheet({ conditions = [], conditionsOp, setDataCheck, + compare, }: createSpendingSpreadsheetProps) { - const [startDate, endDate] = getSpecificRange(3, null, 'Months'); + const thisMonth = monthUtils.subMonths( + monthUtils.currentMonth(), + compare === 'thisMonth' ? 0 : 1, + ); + const [startDate, endDate] = getSpecificRange( + compare === 'thisMonth' ? 3 : 4, + null, + 'Months', + ); const [lastYearStartDate, lastYearEndDate] = getSpecificRange( - 12, - 0, + 13, + 1, 'Months', ); const interval = 'Daily'; @@ -92,6 +102,14 @@ export function createSpendingSpreadsheet({ return { month, perMonthAssets: 0, perMonthDebts: 0 }; }); + months.unshift({ + month: monthUtils.prevYear( + monthUtils.subMonths(monthUtils.currentMonth(), 1), + ), + perMonthAssets: 0, + perMonthDebts: 0, + }); + months.unshift({ month: monthUtils.prevYear(monthUtils.currentMonth()), perMonthAssets: 0, @@ -141,10 +159,22 @@ export function createSpendingSpreadsheet({ }); if ( month.month !== monthUtils.currentMonth() && - month.month !== monthUtils.prevYear(monthUtils.currentMonth()) + month.month !== thisMonth && + month.month !== monthUtils.prevYear(monthUtils.currentMonth()) && + month.month !== + monthUtils.prevYear( + monthUtils.subMonths(monthUtils.currentMonth(), 1), + ) ) { - averageSum += cumulativeAssets + cumulativeDebts; - monthCount += 1; + if (day === '28') { + if (monthUtils.getMonthEnd(intervalItem) === intervalItem) { + averageSum += cumulativeAssets + cumulativeDebts; + monthCount += 1; + } + } else { + averageSum += cumulativeAssets + cumulativeDebts; + monthCount += 1; + } } arr.push({ @@ -181,9 +211,11 @@ export function createSpendingSpreadsheet({ months: indexedData, day, average: integerToAmount(averageSum) / monthCount, - thisMonth: dayData[4].cumulative, - lastMonth: dayData[3].cumulative, + thisMonth: dayData[dayData.length - 1].cumulative, + lastMonth: dayData[dayData.length - 2].cumulative, + twoMonthsPrevious: dayData[dayData.length - 3].cumulative, lastYear: dayData[0].cumulative, + lastYearPrevious: dayData[1].cumulative, }; }); diff --git a/packages/loot-core/src/types/models/reports.d.ts b/packages/loot-core/src/types/models/reports.d.ts index 3fd9a0a424299f1dd8a1ebc55969bae131fd1866..f336dbeb3702a4044a7c5984b11e935bf3d3e163 100644 --- a/packages/loot-core/src/types/models/reports.d.ts +++ b/packages/loot-core/src/types/models/reports.d.ts @@ -32,7 +32,13 @@ export type balanceTypeOpType = | 'netAssets' | 'netDebts'; -export type spendingReportTimeType = 'lastMonth' | 'lastYear' | 'average'; +export type spendingReportTimeType = + | 'average' + | 'thisMonth' + | 'lastMonth' + | 'twoMonthsPrevious' + | 'lastYear' + | 'lastYearPrevious'; export type SpendingMonthEntity = Record< string | number, @@ -59,7 +65,9 @@ export interface SpendingEntity { average: number; thisMonth: number; lastMonth: number; + twoMonthsPrevious: number; lastYear: number; + lastYearPrevious: number; }[]; startDate?: string; endDate?: string; diff --git a/packages/loot-core/src/types/prefs.d.ts b/packages/loot-core/src/types/prefs.d.ts index b859d537f78d8d8a4701cf2a8abb5833ddac3149..2a578a845bb766490ed16aa7073aec45d62b90d0 100644 --- a/packages/loot-core/src/types/prefs.d.ts +++ b/packages/loot-core/src/types/prefs.d.ts @@ -54,6 +54,7 @@ export type LocalPrefs = Partial< reportsViewLabel: boolean; spendingReportFilter: string; spendingReportTime: spendingReportTimeType; + spendingReportCompare: spendingReportTimeType; sidebarWidth: number; 'mobile.showSpentColumn': boolean; } & Record<`flags.${FeatureFlag}`, boolean> diff --git a/upcoming-release-notes/3132.md b/upcoming-release-notes/3132.md new file mode 100644 index 0000000000000000000000000000000000000000..e4067c388f9eeb16a95f83d1244a2f29e6a9e89f --- /dev/null +++ b/upcoming-release-notes/3132.md @@ -0,0 +1,6 @@ +--- +category: Enhancements +authors: [carkom] +--- + +In spending report - adding last month as an option for the primary graph