diff --git a/packages/desktop-client/src/components/reports/graphs/SpendingGraph.tsx b/packages/desktop-client/src/components/reports/graphs/SpendingGraph.tsx index 2aadf407dcf7582a85b63276442120c5762bb220..60b2b4f78ae34fe047926294ecc4e0183d6f046d 100644 --- a/packages/desktop-client/src/components/reports/graphs/SpendingGraph.tsx +++ b/packages/desktop-client/src/components/reports/graphs/SpendingGraph.tsx @@ -45,6 +45,7 @@ type CustomTooltipProps = { payload?: PayloadItem[]; balanceTypeOp?: string; thisMonth?: string; + lastYear?: string; selection?: string; }; @@ -53,6 +54,7 @@ const CustomTooltip = ({ payload, balanceTypeOp, thisMonth, + lastYear, selection, }: CustomTooltipProps) => { if (active && payload && payload.length) { @@ -82,21 +84,27 @@ const CustomTooltip = ({ </strong> </div> <div style={{ lineHeight: 1.5 }}> - {payload[0].payload.months[thisMonth].cumulative && ( + {payload[0].payload.months[thisMonth].cumulative ? ( <AlignedText left="This month:" right={amountToCurrency( payload[0].payload.months[thisMonth].cumulative * -1, )} /> - )} + ) : null} {['cumulative'].includes(balanceTypeOp) && ( <AlignedText - left={selection === 'average' ? 'Average' : 'Last month:'} + left={ + selection === 'average' + ? 'Average' + : selection === lastYear + ? 'Last year' + : 'Last month' + } right={amountToCurrency(comparison)} /> )} - {payload[0].payload.months[thisMonth].cumulative && ( + {payload[0].payload.months[thisMonth].cumulative ? ( <AlignedText left="Difference:" right={amountToCurrency( @@ -104,7 +112,7 @@ const CustomTooltip = ({ comparison, )} /> - )} + ) : null} </div> </div> </div> @@ -129,7 +137,20 @@ export function SpendingGraph({ const balanceTypeOp = 'cumulative'; const thisMonth = monthUtils.currentMonth(); const lastMonth = monthUtils.subMonths(monthUtils.currentMonth(), 1); - const selection = mode.toLowerCase() === 'average' ? 'average' : lastMonth; + const lastYear = monthUtils.prevYear(monthUtils.currentMonth()); + let selection; + switch (mode) { + case 'Average': + selection = 'average'; + break; + case 'lastYear': + selection = lastYear; + break; + default: + selection = lastMonth; + break; + } + const thisMonthMax = data.intervalData.reduce((a, b) => a.months[thisMonth][balanceTypeOp] < b.months[thisMonth][balanceTypeOp] ? a @@ -139,11 +160,11 @@ export function SpendingGraph({ selection === 'average' ? data.intervalData[27].average : data.intervalData.reduce((a, b) => - a.months[lastMonth][balanceTypeOp] < - b.months[lastMonth][balanceTypeOp] + a.months[selection][balanceTypeOp] < + b.months[selection][balanceTypeOp] ? a : b, - ).months[lastMonth][balanceTypeOp]; + ).months[selection][balanceTypeOp]; const maxYAxis = selectionMax > thisMonthMax; const dataMax = Math.max( ...data.intervalData.map(i => i.months[thisMonth].cumulative), @@ -233,6 +254,7 @@ export function SpendingGraph({ <CustomTooltip balanceTypeOp={balanceTypeOp} thisMonth={thisMonth} + lastYear={lastYear} selection={selection} /> } diff --git a/packages/desktop-client/src/components/reports/reports/Spending.tsx b/packages/desktop-client/src/components/reports/reports/Spending.tsx index 5b6dc4aee2463ed21cc344a2294029ddbb4cb666..5f54faaaf7823f8adbf38c1849203a4b27a913f5 100644 --- a/packages/desktop-client/src/components/reports/reports/Spending.tsx +++ b/packages/desktop-client/src/components/reports/reports/Spending.tsx @@ -38,7 +38,7 @@ export function Spending() { } = useFilters<RuleConditionEntity>(); const [dataCheck, setDataCheck] = useState(false); - const [mode, setMode] = useState('Last month'); + const [mode, setMode] = useState('lastMonth'); const getGraphData = useMemo(() => { setDataCheck(false); @@ -57,15 +57,21 @@ export function Spending() { if (!data) { return null; } + const showAverage = - data.intervalData[27].months[ - monthUtils.subMonths(monthUtils.currentDay(), 3) - ].daily !== 0; + Math.abs( + data.intervalData[27].months[ + monthUtils.subMonths(monthUtils.currentDay(), 3) + ].cumulative, + ) > 0; + const todayDay = 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; return ( <Page header={ @@ -171,30 +177,40 @@ export function Spending() { marginBottom: 5, }} > - <AlignedText - left={<Block>Spent MTD:</Block>} - right={ - <Text> - <PrivacyFilter blurIntensity={5}> - {amountToCurrency( - Math.abs(data.intervalData[todayDay].thisMonth), - )} - </PrivacyFilter> - </Text> - } - /> - <AlignedText - left={<Block>Spent Last MTD:</Block>} - right={ - <Text> - <PrivacyFilter blurIntensity={5}> - {amountToCurrency( - Math.abs(data.intervalData[todayDay].lastMonth), - )} - </PrivacyFilter> - </Text> - } - /> + {showLastMonth && ( + <View + style={{ + ...styles.mediumText, + fontWeight: 500, + marginBottom: 5, + }} + > + <AlignedText + left={<Block>Spent MTD:</Block>} + right={ + <Text> + <PrivacyFilter blurIntensity={5}> + {amountToCurrency( + Math.abs(data.intervalData[todayDay].thisMonth), + )} + </PrivacyFilter> + </Text> + } + /> + <AlignedText + left={<Block>Spent Last MTD:</Block>} + right={ + <Text> + <PrivacyFilter blurIntensity={5}> + {amountToCurrency( + Math.abs(data.intervalData[todayDay].lastMonth), + )} + </PrivacyFilter> + </Text> + } + /> + </View> + )} {showAverage && ( <AlignedText left={<Block>Spent Average MTD:</Block>} @@ -211,46 +227,66 @@ export function Spending() { )} </View> </View> - <View - style={{ - alignItems: 'center', - flexDirection: 'row', - }} - > - <Text - style={{ - paddingRight: 10, - }} - > - Compare this month to: - </Text> - <ModeButton - selected={mode === 'Last month'} - onSelect={() => setMode('Last month')} - > - Last month - </ModeButton> - {showAverage && ( - <ModeButton - selected={mode === 'Average'} - onSelect={() => setMode('Average')} + {!showLastMonth ? ( + <View style={{ marginTop: 30 }}> + <h1>Additional data required to generate graph</h1> + <Paragraph> + Currently, there is insufficient data to display any + information regarding your spending. Please input + transactions from last month to enable graph visualization. + </Paragraph> + </View> + ) : ( + <> + <View + style={{ + alignItems: 'center', + flexDirection: 'row', + }} > - Average - </ModeButton> - )} - </View> + <Text + style={{ + paddingRight: 10, + }} + > + Compare this month to: + </Text> + <ModeButton + selected={mode === 'lastMonth'} + onSelect={() => setMode('lastMonth')} + > + Last month + </ModeButton> + {showLastYear && ( + <ModeButton + selected={mode === 'lastYear'} + onSelect={() => setMode('lastYear')} + > + Last year + </ModeButton> + )} + {showAverage && ( + <ModeButton + selected={mode === 'Average'} + onSelect={() => setMode('Average')} + > + Average + </ModeButton> + )} + </View> - {dataCheck ? ( - <SpendingGraph - style={{ flexGrow: 1 }} - compact={false} - data={data} - mode={mode} - /> - ) : ( - <LoadingIndicator message="Loading report..." /> + {dataCheck ? ( + <SpendingGraph + style={{ flexGrow: 1 }} + compact={false} + data={data} + mode={mode} + /> + ) : ( + <LoadingIndicator message="Loading report..." /> + )} + </> )} - {showAverage && ( <View style={{ marginTop: 30 }}> <Paragraph> diff --git a/packages/desktop-client/src/components/reports/reports/SpendingCard.tsx b/packages/desktop-client/src/components/reports/reports/SpendingCard.tsx index 8a11dd3428ceaa4dde453d217c7dbe07fe016261..d999494ed9b00e7be5b0f211040f096e4d4a0b08 100644 --- a/packages/desktop-client/src/components/reports/reports/SpendingCard.tsx +++ b/packages/desktop-client/src/components/reports/reports/SpendingCard.tsx @@ -36,6 +36,7 @@ export function SpendingCard() { data && data.intervalData[todayDay].lastMonth - data.intervalData[todayDay].thisMonth; + const showLastMonth = data && Math.abs(data.intervalData[27].lastMonth) > 0; return ( <ReportCard flex="1" to="/reports/spending"> @@ -57,7 +58,7 @@ export function SpendingCard() { end={monthUtils.currentMonth()} /> </View> - {data && ( + {data && showLastMonth && ( <View style={{ textAlign: 'right' }}> <Block style={{ @@ -80,8 +81,13 @@ export function SpendingCard() { </View> )} </View> - - {data ? ( + {!showLastMonth ? ( + <View style={{ padding: 5 }}> + <p style={{ margin: 0, textAlign: 'center' }}> + Additional data required to generate graph + </p> + </View> + ) : data ? ( <SpendingGraph style={{ flex: 1 }} compact={true} @@ -89,7 +95,7 @@ export function SpendingCard() { mode="lastMonth" /> ) : ( - <LoadingIndicator /> + <LoadingIndicator message="Loading report..." /> )} </View> </ReportCard> 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 79981b47a77b9b118dd98347c262a3a006fc7709..205a1b160b3a1f1bf157a4c9478a83a8a47ffa80 100644 --- a/packages/desktop-client/src/components/reports/spreadsheets/spending-spreadsheet.ts +++ b/packages/desktop-client/src/components/reports/spreadsheets/spending-spreadsheet.ts @@ -1,5 +1,4 @@ // @ts-strict-ignore - import keyBy from 'lodash/keyBy'; import { runQuery } from 'loot-core/src/client/query-helpers'; @@ -35,6 +34,11 @@ export function createSpendingSpreadsheet({ setDataCheck, }: createSpendingSpreadsheetProps) { const [startDate, endDate] = getSpecificRange(3, null, 'Months'); + const [lastYearStartDate, lastYearEndDate] = getSpecificRange( + 12, + 0, + 'Months', + ); const interval = 'Daily'; return async ( @@ -50,7 +54,7 @@ export function createSpendingSpreadsheet({ runQuery( makeQuery( 'assets', - startDate, + lastYearStartDate, endDate, interval, categories.list, @@ -61,7 +65,7 @@ export function createSpendingSpreadsheet({ runQuery( makeQuery( 'debts', - startDate, + lastYearStartDate, endDate, interval, categories.list, @@ -72,6 +76,9 @@ export function createSpendingSpreadsheet({ ]); const intervals = monthUtils.dayRangeInclusive(startDate, endDate); + intervals.push( + ...monthUtils.dayRangeInclusive(lastYearStartDate, lastYearEndDate), + ); const days = [...Array(29).keys()] .filter(f => f > 0) .map(n => n.toString().padStart(2, '0')); @@ -85,6 +92,12 @@ export function createSpendingSpreadsheet({ return { month, perMonthAssets: 0, perMonthDebts: 0 }; }); + months.unshift({ + month: monthUtils.prevYear(monthUtils.currentMonth()), + perMonthAssets: 0, + perMonthDebts: 0, + }); + const intervalData = days.map(day => { let averageSum = 0; let monthCount = 0; @@ -126,7 +139,10 @@ export function createSpendingSpreadsheet({ } return null; }); - if (month.month !== monthUtils.currentMonth()) { + if ( + month.month !== monthUtils.currentMonth() && + month.month !== monthUtils.prevYear(monthUtils.currentMonth()) + ) { averageSum += cumulativeAssets + cumulativeDebts; monthCount += 1; } @@ -165,8 +181,9 @@ export function createSpendingSpreadsheet({ months: indexedData, day, average: integerToAmount(averageSum) / monthCount, - thisMonth: dayData[3].cumulative, - lastMonth: dayData[2].cumulative, + thisMonth: dayData[4].cumulative, + lastMonth: dayData[3].cumulative, + lastYear: dayData[0].cumulative, }; }); diff --git a/packages/loot-core/src/types/models/reports.d.ts b/packages/loot-core/src/types/models/reports.d.ts index eccc1c79f99816f76cce4fc98f042110a5c87324..42ed96875b387f71c769078b481bb0c8f5671cab 100644 --- a/packages/loot-core/src/types/models/reports.d.ts +++ b/packages/loot-core/src/types/models/reports.d.ts @@ -49,6 +49,7 @@ export interface SpendingEntity { average: number; thisMonth: number; lastMonth: number; + lastYear: number; }[]; startDate?: string; endDate?: string; diff --git a/upcoming-release-notes/2806.md b/upcoming-release-notes/2806.md new file mode 100644 index 0000000000000000000000000000000000000000..bfeb7b8b1d57cc49a101551d988dafebbef3e374 --- /dev/null +++ b/upcoming-release-notes/2806.md @@ -0,0 +1,6 @@ +--- +category: Features +authors: [Crazypkr1099] +--- + +Add Year Spending Comparison Feature