From 16944a614097fef3571da9a8f034602a99220e15 Mon Sep 17 00:00:00 2001
From: Neil <55785687+carkom@users.noreply.github.com>
Date: Wed, 11 Sep 2024 10:58:59 +0100
Subject: [PATCH] Spending Report: Header update (#3380)

* adjust header

* notes

* typecheck fixes

* fix bugs and UI changes

* fix error for invalid time

* change budget button wording

* fix some visual issues
---
 .../src/components/reports/DateRange.tsx      |  17 +-
 .../reports/graphs/SpendingGraph.tsx          |  93 ++-----
 .../components/reports/reports/Spending.tsx   | 257 +++++++++++-------
 .../reports/reports/SpendingCard.tsx          |  44 ++-
 .../spreadsheets/spending-spreadsheet.ts      | 145 ++++++----
 .../loot-core/src/types/models/reports.d.ts   |  16 +-
 packages/loot-core/src/types/prefs.d.ts       |   7 +-
 upcoming-release-notes/3380.md                |   6 +
 8 files changed, 335 insertions(+), 250 deletions(-)
 create mode 100644 upcoming-release-notes/3380.md

diff --git a/packages/desktop-client/src/components/reports/DateRange.tsx b/packages/desktop-client/src/components/reports/DateRange.tsx
index e2ac6d03c..faefe46e9 100644
--- a/packages/desktop-client/src/components/reports/DateRange.tsx
+++ b/packages/desktop-client/src/components/reports/DateRange.tsx
@@ -10,6 +10,7 @@ import { Text } from '../common/Text';
 type DateRangeProps = {
   start: string;
   end: string;
+  type?: string;
 };
 
 function checkDate(date: string) {
@@ -21,7 +22,7 @@ function checkDate(date: string) {
   }
 }
 
-export function DateRange({ start, end }: DateRangeProps): ReactElement {
+export function DateRange({ start, end, type }: DateRangeProps): ReactElement {
   const checkStart = checkDate(start);
   const checkEnd = checkDate(end);
 
@@ -42,13 +43,23 @@ export function DateRange({ start, end }: DateRangeProps): ReactElement {
   if (startDate.getFullYear() !== endDate.getFullYear()) {
     content = (
       <div>
-        {d.format(startDate, 'MMM yyyy')} - {d.format(endDate, 'MMM yyyy')}
+        {type && 'Compare '}
+        {d.format(startDate, 'MMM yyyy')}
+        {type ? ' to ' : ' - '}
+        {['budget', 'average'].includes(type || '')
+          ? type
+          : d.format(endDate, 'MMM yyyy')}
       </div>
     );
   } else if (startDate.getMonth() !== endDate.getMonth()) {
     content = (
       <div>
-        {d.format(startDate, 'MMM yyyy')} - {d.format(endDate, 'MMM yyyy')}
+        {type && 'Compare '}
+        {d.format(startDate, 'MMM yyyy')}
+        {type ? ' to ' : ' - '}
+        {['budget', 'average'].includes(type || '')
+          ? type
+          : d.format(endDate, 'MMM yyyy')}
       </div>
     );
   } else {
diff --git a/packages/desktop-client/src/components/reports/graphs/SpendingGraph.tsx b/packages/desktop-client/src/components/reports/graphs/SpendingGraph.tsx
index a035c919b..4c6660e40 100644
--- a/packages/desktop-client/src/components/reports/graphs/SpendingGraph.tsx
+++ b/packages/desktop-client/src/components/reports/graphs/SpendingGraph.tsx
@@ -13,7 +13,6 @@ import {
   ResponsiveContainer,
 } from 'recharts';
 
-import * as monthUtils from 'loot-core/src/shared/months';
 import {
   amountToCurrency,
   amountToCurrencyNoDecimal,
@@ -44,8 +43,6 @@ type CustomTooltipProps = {
   active?: boolean;
   payload?: PayloadItem[];
   balanceTypeOp?: string;
-  thisMonth?: string;
-  lastYear?: string;
   selection?: string;
   compare?: string;
 };
@@ -54,18 +51,15 @@ const CustomTooltip = ({
   active,
   payload,
   balanceTypeOp,
-  thisMonth,
-  lastYear,
   selection,
   compare,
 }: CustomTooltipProps) => {
   const { t } = useTranslation();
 
   if (active && payload && payload.length) {
-    const comparison =
-      selection === 'average'
-        ? payload[0].payload[selection] * -1
-        : payload[0].payload.months[selection].cumulative * -1;
+    const comparison = ['average', 'budget'].includes(selection)
+      ? payload[0].payload[selection] * -1
+      : payload[0].payload.months[selection].cumulative * -1;
     return (
       <div
         className={`${css({
@@ -88,13 +82,11 @@ const CustomTooltip = ({
             </strong>
           </div>
           <div style={{ lineHeight: 1.5 }}>
-            {payload[0].payload.months[thisMonth].cumulative ? (
+            {payload[0].payload.months[compare].cumulative ? (
               <AlignedText
-                left={
-                  compare === 'thisMonth' ? t('This month:') : t('Last month:')
-                }
+                left={t('Compare:')}
                 right={amountToCurrency(
-                  payload[0].payload.months[thisMonth].cumulative * -1,
+                  payload[0].payload.months[compare].cumulative * -1,
                 )}
               />
             ) : null}
@@ -103,20 +95,18 @@ const CustomTooltip = ({
                 left={
                   selection === 'average'
                     ? t('Average:')
-                    : selection === lastYear
-                      ? t('Last year:')
-                      : compare === 'thisMonth'
-                        ? t('Last month:')
-                        : t('2 months ago:')
+                    : selection === 'budget'
+                      ? t('Budgeted:')
+                      : t('To:')
                 }
                 right={amountToCurrency(comparison)}
               />
             )}
-            {payload[0].payload.months[thisMonth].cumulative ? (
+            {payload[0].payload.months[compare].cumulative ? (
               <AlignedText
                 left={t('Difference:')}
                 right={amountToCurrency(
-                  payload[0].payload.months[thisMonth].cumulative * -1 -
+                  payload[0].payload.months[compare].cumulative * -1 -
                     comparison,
                 )}
               />
@@ -134,6 +124,7 @@ type SpendingGraphProps = {
   compact?: boolean;
   mode: string;
   compare: string;
+  compareTo: string;
 };
 
 export function SpendingGraph({
@@ -142,51 +133,29 @@ export function SpendingGraph({
   compact,
   mode,
   compare,
+  compareTo,
 }: SpendingGraphProps) {
   const privacyMode = usePrivacyMode();
   const balanceTypeOp = 'cumulative';
-  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':
-      selection = 'average';
-      break;
-    case 'lastYear':
-      selection = lastYear;
-      break;
-    default:
-      selection = previousMonth;
-      break;
-  }
+
+  const selection = mode === 'singleMonth' ? compareTo : mode;
 
   const thisMonthMax = data.intervalData.reduce((a, b) =>
-    a.months[thisMonth][balanceTypeOp] < b.months[thisMonth][balanceTypeOp]
-      ? a
-      : b,
-  ).months[thisMonth][balanceTypeOp];
-  const selectionMax =
-    selection === 'average'
-      ? data.intervalData[27].average
-      : data.intervalData.reduce((a, b) =>
-          a.months[selection][balanceTypeOp] <
-          b.months[selection][balanceTypeOp]
-            ? a
-            : b,
-        ).months[selection][balanceTypeOp];
+    a.months[compare][balanceTypeOp] < b.months[compare][balanceTypeOp] ? a : b,
+  ).months[compare][balanceTypeOp];
+  const selectionMax = ['average', 'budget'].includes(selection)
+    ? data.intervalData[27][selection]
+    : data.intervalData.reduce((a, b) =>
+        a.months[selection][balanceTypeOp] < b.months[selection][balanceTypeOp]
+          ? a
+          : b,
+      ).months[selection][balanceTypeOp];
   const maxYAxis = selectionMax > thisMonthMax;
   const dataMax = Math.max(
-    ...data.intervalData.map(i => i.months[thisMonth].cumulative),
+    ...data.intervalData.map(i => i.months[compare].cumulative),
   );
   const dataMin = Math.min(
-    ...data.intervalData.map(i => i.months[thisMonth].cumulative),
+    ...data.intervalData.map(i => i.months[compare].cumulative),
   );
 
   const tickFormatter = tick => {
@@ -206,7 +175,7 @@ export function SpendingGraph({
   };
 
   const getVal = (obj, month) => {
-    if (month === 'average') {
+    if (['average', 'budget'].includes(month)) {
       return obj[month] && -1 * obj[month];
     } else {
       return (
@@ -255,9 +224,7 @@ export function SpendingGraph({
                 )}
                 {compact ? null : (
                   <YAxis
-                    dataKey={val =>
-                      getVal(val, maxYAxis ? thisMonth : selection)
-                    }
+                    dataKey={val => getVal(val, maxYAxis ? compare : selection)}
                     domain={[0, 'auto']}
                     tickFormatter={tickFormatter}
                     tick={{ fill: theme.pageText }}
@@ -269,8 +236,6 @@ export function SpendingGraph({
                   content={
                     <CustomTooltip
                       balanceTypeOp={balanceTypeOp}
-                      thisMonth={thisMonth}
-                      lastYear={lastYear}
                       selection={selection}
                       compare={compare}
                     />
@@ -316,7 +281,7 @@ export function SpendingGraph({
                     r: 10,
                   }}
                   animationDuration={0}
-                  dataKey={val => getVal(val, thisMonth)}
+                  dataKey={val => getVal(val, compare)}
                   stroke={`url(#stroke${balanceTypeOp})`}
                   strokeWidth={3}
                   fill={`url(#fill${balanceTypeOp})`}
diff --git a/packages/desktop-client/src/components/reports/reports/Spending.tsx b/packages/desktop-client/src/components/reports/reports/Spending.tsx
index 7301a1098..ec6c81008 100644
--- a/packages/desktop-client/src/components/reports/reports/Spending.tsx
+++ b/packages/desktop-client/src/components/reports/reports/Spending.tsx
@@ -1,5 +1,8 @@
 import React, { useState, useMemo, useEffect } from 'react';
 
+import * as d from 'date-fns';
+
+import { send } from 'loot-core/src/platform/client/fetch';
 import * as monthUtils from 'loot-core/src/shared/months';
 import { amountToCurrency } from 'loot-core/src/shared/util';
 import { type RuleConditionEntity } from 'loot-core/types/models/rule';
@@ -27,6 +30,7 @@ import { LoadingIndicator } from '../LoadingIndicator';
 import { ModeButton } from '../ModeButton';
 import { createSpendingSpreadsheet } from '../spreadsheets/spending-spreadsheet';
 import { useReport } from '../useReport';
+import { fromDateRepr } from '../util';
 
 export function Spending() {
   const {
@@ -38,26 +42,68 @@ export function Spending() {
     onConditionsOpChange,
   } = useFilters<RuleConditionEntity>();
 
+  const emptyIntervals: { name: string; pretty: string }[] = [];
+  const [allIntervals, setAllIntervals] = useState(emptyIntervals);
+
   const [spendingReportFilter = '', setSpendingReportFilter] = useLocalPref(
     'spendingReportFilter',
   );
-  const [spendingReportTime = 'lastMonth', setSpendingReportTime] =
-    useLocalPref('spendingReportTime');
-  const [spendingReportCompare = 'thisMonth', setSpendingReportCompare] =
-    useLocalPref('spendingReportCompare');
+  const [spendingReportMode = 'singleMonth', setSpendingReportMode] =
+    useLocalPref('spendingReportMode');
+  const [
+    spendingReportCompare = monthUtils.currentMonth(),
+    setSpendingReportCompare,
+  ] = useLocalPref('spendingReportCompare');
+  const [
+    spendingReportCompareTo = monthUtils.currentMonth(),
+    setSpendingReportCompareTo,
+  ] = useLocalPref('spendingReportCompareTo');
 
+  const isDateValid = monthUtils.parseDate(spendingReportCompare);
   const [dataCheck, setDataCheck] = useState(false);
-  const [compare, setCompare] = useState(spendingReportCompare);
-  const [mode, setMode] = useState(spendingReportTime);
+  const [mode, setMode] = useState(spendingReportMode);
+  const [compare, setCompare] = useState(
+    isDateValid.toString() === 'Invalid Date'
+      ? monthUtils.currentMonth()
+      : spendingReportCompare,
+  );
+  const [compareTo, setCompareTo] = useState(spendingReportCompareTo);
 
   const parseFilter = spendingReportFilter && JSON.parse(spendingReportFilter);
   const filterSaved =
     JSON.stringify(parseFilter.conditions) === JSON.stringify(conditions) &&
     parseFilter.conditionsOp === conditionsOp &&
-    spendingReportTime === mode &&
-    spendingReportCompare === compare;
+    spendingReportMode === mode &&
+    spendingReportCompare === compare &&
+    spendingReportCompareTo === compareTo;
 
   useEffect(() => {
+    async function run() {
+      const trans = await send('get-earliest-transaction');
+
+      let earliestMonth = trans
+        ? monthUtils.monthFromDate(d.parseISO(fromDateRepr(trans.date)))
+        : monthUtils.currentMonth();
+
+      // Make sure the month selects are at least populates with a
+      // year's worth of months. We can undo this when we have fancier
+      // date selects.
+      const yearAgo = monthUtils.subMonths(monthUtils.currentMonth(), 12);
+      if (earliestMonth > yearAgo) {
+        earliestMonth = yearAgo;
+      }
+
+      const allMonths = monthUtils
+        .rangeInclusive(earliestMonth, monthUtils.currentMonth())
+        .map(month => ({
+          name: month,
+          pretty: monthUtils.format(month, 'MMMM, yyyy'),
+        }))
+        .reverse();
+
+      setAllIntervals(allMonths);
+    }
+    run();
     const checkFilter =
       spendingReportFilter && JSON.parse(spendingReportFilter);
     if (checkFilter.conditions) {
@@ -72,8 +118,9 @@ export function Spending() {
       conditionsOp,
       setDataCheck,
       compare,
+      compareTo,
     });
-  }, [conditions, conditionsOp, compare]);
+  }, [conditions, conditionsOp, compare, compareTo]);
 
   const data = useReport('default', getGraphData);
   const navigate = useNavigate();
@@ -90,32 +137,28 @@ export function Spending() {
         conditions,
       }),
     );
-    setSpendingReportTime(mode);
+    setSpendingReportMode(mode);
     setSpendingReportCompare(compare);
+    setSpendingReportCompareTo(compareTo);
   };
 
   const showAverage =
+    data.intervalData[27].months[monthUtils.subMonths(compare, 3)] &&
     Math.abs(
-      data.intervalData[27].months[
-        monthUtils.subMonths(monthUtils.currentDay(), 3)
-      ].cumulative,
+      data.intervalData[27].months[monthUtils.subMonths(compare, 3)].cumulative,
     ) > 0;
 
   const todayDay =
-    compare === 'lastMonth'
+    compare !== monthUtils.currentMonth()
       ? 27
       : monthUtils.getDay(monthUtils.currentDay()) - 1 >= 28
         ? 27
         : monthUtils.getDay(monthUtils.currentDay()) - 1;
 
-  const showLastYear =
-    Math.abs(
-      data.intervalData[27][
-        compare === 'thisMonth' ? 'lastYear' : 'lastYearPrevious'
-      ],
-    ) > 0;
-  const showPreviousMonth =
-    Math.abs(data.intervalData[27][spendingReportTime]) > 0;
+  const showCompareTo = Math.abs(data.intervalData[27].compareTo) > 0;
+  const showCompare =
+    compare === monthUtils.currentMonth() ||
+    Math.abs(data.intervalData[27].compare) > 0;
   return (
     <Page
       header={
@@ -161,23 +204,37 @@ export function Spending() {
             value={compare}
             onChange={e => {
               setCompare(e);
-              if (mode === 'lastMonth') setMode('twoMonthsPrevious');
-              if (mode === 'twoMonthsPrevious') setMode('lastMonth');
             }}
-            options={[
-              ['thisMonth', 'this month'],
-              ['lastMonth', 'last month'],
-            ]}
+            options={allIntervals.map(({ name, pretty }) => [name, pretty])}
           />
           <Text
             style={{
-              paddingRight: 10,
+              paddingRight: 5,
               paddingLeft: 5,
             }}
           >
-            to the:
+            to
           </Text>
+          <Select
+            value={compareTo}
+            onChange={e => {
+              setCompareTo(e);
+            }}
+            options={allIntervals.map(({ name, pretty }) => [name, pretty])}
+            disabled={mode !== 'singleMonth'}
+          />
         </View>
+        {!isNarrowWidth && (
+          <View
+            style={{
+              width: 1,
+              height: 30,
+              backgroundColor: theme.pillBorderDark,
+              marginRight: 15,
+              marginLeft: 10,
+            }}
+          />
+        )}
         <View
           style={{
             flexDirection: 'row',
@@ -187,52 +244,42 @@ export function Spending() {
           }}
         >
           <ModeButton
-            selected={['lastMonth', 'twoMonthsPrevious'].includes(mode)}
+            selected={mode === 'singleMonth'}
             style={{
               backgroundColor: 'inherit',
             }}
-            onSelect={() =>
-              setMode(
-                compare === 'thisMonth' ? 'lastMonth' : 'twoMonthsPrevious',
-              )
-            }
+            onSelect={() => setMode('singleMonth')}
           >
-            Previous month
+            Single month
+          </ModeButton>
+          <ModeButton
+            selected={mode === 'budget'}
+            onSelect={() => setMode('budget')}
+            style={{
+              backgroundColor: 'inherit',
+            }}
+          >
+            Budgeted
+          </ModeButton>
+          <ModeButton
+            selected={mode === 'average'}
+            onSelect={() => setMode('average')}
+            style={{
+              backgroundColor: 'inherit',
+            }}
+          >
+            Average
           </ModeButton>
-          {showLastYear && (
-            <ModeButton
-              selected={mode === 'lastYear'}
-              onSelect={() => setMode('lastYear')}
-              style={{
-                backgroundColor: 'inherit',
-              }}
-            >
-              Last year
-            </ModeButton>
-          )}
-          {showAverage && (
-            <ModeButton
-              selected={mode === 'average'}
-              onSelect={() => setMode('average')}
-              style={{
-                backgroundColor: 'inherit',
-              }}
-            >
-              Average
-            </ModeButton>
-          )}
         </View>
         {!isNarrowWidth && (
-          <>
-            <View
-              style={{
-                width: 1,
-                height: 30,
-                backgroundColor: theme.pillBorderDark,
-                marginRight: 10,
-              }}
-            />{' '}
-          </>
+          <View
+            style={{
+              width: 1,
+              height: 30,
+              backgroundColor: theme.pillBorderDark,
+              marginRight: 10,
+            }}
+          />
         )}
         <View
           style={{
@@ -251,7 +298,7 @@ export function Spending() {
           />
           <View style={{ flex: 1 }} />
           <Tooltip
-            placement="bottom start"
+            placement="top end"
             content={<Text>Save compare and filter options</Text>}
             style={{
               ...styles.tooltip,
@@ -335,67 +382,72 @@ export function Spending() {
                     color: theme.pageText,
                   }}
                 >
-                  {showPreviousMonth && (
-                    <View>
+                  <View>
+                    {showCompareTo && (
                       <AlignedText
                         style={{ marginBottom: 5, minWidth: 210 }}
                         left={
                           <Block>
-                            Spent{' '}
-                            {compare === 'thisMonth' ? 'MTD' : 'Last Month'}:
+                            Spent {monthUtils.format(compare, 'MMM, yyyy')}
+                            {compare === monthUtils.currentMonth() && ' MTD'}:
                           </Block>
                         }
                         right={
                           <Text style={{ fontWeight: 600 }}>
                             <PrivacyFilter blurIntensity={5}>
                               {amountToCurrency(
-                                Math.abs(
-                                  data.intervalData[todayDay][
-                                    compare === 'thisMonth'
-                                      ? 'thisMonth'
-                                      : 'lastMonth'
-                                  ],
-                                ),
+                                Math.abs(data.intervalData[todayDay].compare),
                               )}
                             </PrivacyFilter>
                           </Text>
                         }
                       />
+                    )}
+                    {mode === 'singleMonth' && (
                       <AlignedText
                         style={{ marginBottom: 5, minWidth: 210 }}
                         left={
                           <Block>
-                            Spent{' '}
-                            {compare === 'thisMonth'
-                              ? ' Last MTD'
-                              : '2 Months Ago'}
-                            :
+                            Spent {monthUtils.format(compareTo, 'MMM, yyyy')}:
                           </Block>
                         }
                         right={
                           <Text style={{ fontWeight: 600 }}>
                             <PrivacyFilter blurIntensity={5}>
                               {amountToCurrency(
-                                Math.abs(
-                                  data.intervalData[todayDay][
-                                    compare === 'thisMonth'
-                                      ? 'lastMonth'
-                                      : 'twoMonthsPrevious'
-                                  ],
-                                ),
+                                Math.abs(data.intervalData[todayDay].compareTo),
                               )}
                             </PrivacyFilter>
                           </Text>
                         }
                       />
-                    </View>
-                  )}
+                    )}
+                  </View>
+                  <AlignedText
+                    style={{ marginBottom: 5, minWidth: 210 }}
+                    left={
+                      <Block>
+                        Budgeted
+                        {compare === monthUtils.currentMonth() && ' MTD'}:
+                      </Block>
+                    }
+                    right={
+                      <Text style={{ fontWeight: 600 }}>
+                        <PrivacyFilter blurIntensity={5}>
+                          {amountToCurrency(
+                            Math.abs(data.intervalData[todayDay].budget),
+                          )}
+                        </PrivacyFilter>
+                      </Text>
+                    }
+                  />
                   {showAverage && (
                     <AlignedText
                       style={{ marginBottom: 5, minWidth: 210 }}
                       left={
                         <Block>
-                          Spent Average{compare === 'thisMonth' && ' MTD'}:
+                          Spent Average
+                          {compare === monthUtils.currentMonth() && ' MTD'}:
                         </Block>
                       }
                       right={
@@ -411,13 +463,15 @@ export function Spending() {
                   )}
                 </View>
               </View>
-              {!showPreviousMonth ? (
+              {!showCompare ||
+              (mode === 'singleMonth' && !showCompareTo) ||
+              (mode === 'average' && !showAverage) ? (
                 <View style={{ marginTop: 20 }}>
                   <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.
+                    Currently, there is insufficient data to display selected
+                    information regarding your spending. Please adjust selection
+                    options to enable graph visualization.
                   </Paragraph>
                 </View>
               ) : dataCheck ? (
@@ -427,6 +481,7 @@ export function Spending() {
                   data={data}
                   mode={mode}
                   compare={compare}
+                  compareTo={compareTo}
                 />
               ) : (
                 <LoadingIndicator message="Loading report..." />
@@ -440,7 +495,7 @@ export function Spending() {
                   </Paragraph>
                   <Paragraph>
                     They are both the average cumulative spending by day for the
-                    last three months.
+                    three months before the selected “compare” month.
                   </Paragraph>
                 </View>
               )}
diff --git a/packages/desktop-client/src/components/reports/reports/SpendingCard.tsx b/packages/desktop-client/src/components/reports/reports/SpendingCard.tsx
index 3e99ac1ed..dd3cffe06 100644
--- a/packages/desktop-client/src/components/reports/reports/SpendingCard.tsx
+++ b/packages/desktop-client/src/components/reports/reports/SpendingCard.tsx
@@ -39,34 +39,50 @@ export function SpendingCard({
 
   const [isCardHovered, setIsCardHovered] = useState(false);
   const [spendingReportFilter = ''] = useLocalPref('spendingReportFilter');
-  const [spendingReportTime = 'lastMonth'] = useLocalPref('spendingReportTime');
-  const [spendingReportCompare = 'thisMonth'] = useLocalPref(
+  const [spendingReportMode = 'singleMonth'] =
+    useLocalPref('spendingReportMode');
+  const [spendingReportCompare = monthUtils.currentMonth()] = useLocalPref(
     'spendingReportCompare',
   );
+  const [spendingReportCompareTo = monthUtils.currentMonth()] = useLocalPref(
+    'spendingReportCompareTo',
+  );
 
   const [nameMenuOpen, setNameMenuOpen] = useState(false);
 
+  const selection =
+    spendingReportMode === 'singleMonth' ? 'compareTo' : spendingReportMode;
   const parseFilter = spendingReportFilter && JSON.parse(spendingReportFilter);
+  const isDateValid = monthUtils.parseDate(spendingReportCompare);
   const getGraphData = useMemo(() => {
     return createSpendingSpreadsheet({
       conditions: parseFilter.conditions,
       conditionsOp: parseFilter.conditionsOp,
-      compare: spendingReportCompare,
+      compare:
+        isDateValid.toString() === 'Invalid Date'
+          ? monthUtils.currentMonth()
+          : spendingReportCompare,
+      compareTo: spendingReportCompareTo,
     });
-  }, [parseFilter, spendingReportCompare]);
+  }, [
+    parseFilter,
+    spendingReportCompare,
+    spendingReportCompareTo,
+    isDateValid,
+  ]);
 
   const data = useReport('default', getGraphData);
   const todayDay =
-    spendingReportCompare === 'lastMonth'
+    spendingReportCompare !== monthUtils.currentMonth()
       ? 27
       : monthUtils.getDay(monthUtils.currentDay()) - 1 >= 28
         ? 27
         : monthUtils.getDay(monthUtils.currentDay()) - 1;
   const difference =
     data &&
-    data.intervalData[todayDay][spendingReportTime] -
-      data.intervalData[todayDay][spendingReportCompare];
-  const showLastMonth = data && Math.abs(data.intervalData[27].lastMonth) > 0;
+    data.intervalData[todayDay][selection] -
+      data.intervalData[todayDay].compare;
+  const showCompareTo = data && Math.abs(data.intervalData[27].compareTo) > 0;
 
   const spendingReportFeatureFlag = useFeatureFlag('spendingReport');
 
@@ -126,11 +142,12 @@ export function SpendingCard({
               onClose={() => setNameMenuOpen(false)}
             />
             <DateRange
-              start={monthUtils.addMonths(monthUtils.currentMonth(), 1)}
-              end={monthUtils.addMonths(monthUtils.currentMonth(), 1)}
+              start={spendingReportCompare}
+              end={spendingReportCompareTo}
+              type={spendingReportMode}
             />
           </View>
-          {data && showLastMonth && (
+          {data && showCompareTo && (
             <View style={{ textAlign: 'right' }}>
               <Block
                 style={{
@@ -153,7 +170,7 @@ export function SpendingCard({
             </View>
           )}
         </View>
-        {!showLastMonth ? (
+        {!showCompareTo || isDateValid.toString() === 'Invalid Date' ? (
           <View style={{ padding: 5 }}>
             <p style={{ margin: 0, textAlign: 'center' }}>
               <Trans>Additional data required to generate graph</Trans>
@@ -164,8 +181,9 @@ export function SpendingCard({
             style={{ flex: 1 }}
             compact={true}
             data={data}
-            mode={spendingReportTime}
+            mode={spendingReportMode}
             compare={spendingReportCompare}
+            compareTo={spendingReportCompareTo}
           />
         ) : (
           <LoadingIndicator message={t('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 7324405d2..0ffa6d742 100644
--- a/packages/desktop-client/src/components/reports/spreadsheets/spending-spreadsheet.ts
+++ b/packages/desktop-client/src/components/reports/spreadsheets/spending-spreadsheet.ts
@@ -5,6 +5,7 @@ import { runQuery } from 'loot-core/src/client/query-helpers';
 import { type useSpreadsheet } from 'loot-core/src/client/SpreadsheetProvider';
 import { send } from 'loot-core/src/platform/client/fetch';
 import * as monthUtils from 'loot-core/src/shared/months';
+import { q } from 'loot-core/src/shared/query';
 import { integerToAmount } from 'loot-core/src/shared/util';
 import { type RuleConditionEntity } from 'loot-core/src/types/models';
 import {
@@ -12,8 +13,6 @@ import {
   type SpendingEntity,
 } from 'loot-core/src/types/models/reports';
 
-import { getSpecificRange } from '../reportRanges';
-
 import { makeQuery } from './makeQuery';
 
 type createSpendingSpreadsheetProps = {
@@ -21,6 +20,7 @@ type createSpendingSpreadsheetProps = {
   conditionsOp?: string;
   setDataCheck?: (value: boolean) => void;
   compare?: string;
+  compareTo?: string;
 };
 
 export function createSpendingSpreadsheet({
@@ -28,22 +28,17 @@ export function createSpendingSpreadsheet({
   conditionsOp,
   setDataCheck,
   compare,
+  compareTo,
 }: createSpendingSpreadsheetProps) {
-  const thisMonth = monthUtils.subMonths(
-    monthUtils.currentMonth(),
-    compare === 'thisMonth' ? 0 : 1,
-  );
-  const [startDate, endDate] = getSpecificRange(
-    compare === 'thisMonth' ? 3 : 4,
-    null,
-    'Months',
-  );
-  const [lastYearStartDate, lastYearEndDate] = getSpecificRange(
-    13,
-    1,
-    'Months',
-  );
+  const startDate = monthUtils.subMonths(compare, 3) + '-01';
+  const endDate = monthUtils.getMonthEnd(compare + '-01');
+  const startDateTo = compareTo + '-01';
+  const endDateTo = monthUtils.getMonthEnd(compareTo + '-01');
   const interval = 'Daily';
+  const compareInterval = monthUtils.dayRangeInclusive(
+    compare + '-01',
+    endDate,
+  );
 
   return async (
     spreadsheet: ReturnType<typeof useSpreadsheet>,
@@ -58,7 +53,7 @@ export function createSpendingSpreadsheet({
       runQuery(
         makeQuery(
           'assets',
-          lastYearStartDate,
+          startDate,
           endDate,
           interval,
           conditionsOpKey,
@@ -68,7 +63,7 @@ export function createSpendingSpreadsheet({
       runQuery(
         makeQuery(
           'debts',
-          lastYearStartDate,
+          startDate,
           endDate,
           interval,
           conditionsOpKey,
@@ -77,36 +72,84 @@ export function createSpendingSpreadsheet({
       ).then(({ data }) => data),
     ]);
 
+    const [assetsTo, debtsTo] = await Promise.all([
+      runQuery(
+        makeQuery(
+          'assets',
+          startDateTo,
+          endDateTo,
+          interval,
+          conditionsOpKey,
+          filters,
+        ),
+      ).then(({ data }) => data),
+      runQuery(
+        makeQuery(
+          'debts',
+          startDateTo,
+          endDateTo,
+          interval,
+          conditionsOpKey,
+          filters,
+        ),
+      ).then(({ data }) => data),
+    ]);
+
+    const overlapAssets =
+      endDateTo < startDate || startDateTo > endDate ? assetsTo : [];
+    const overlapDebts =
+      endDateTo < startDate || startDateTo > endDate ? debtsTo : [];
+
+    const combineAssets = [...assets, ...overlapAssets];
+    const combineDebts = [...debts, ...overlapDebts];
+
+    const budgetMonth = parseInt(compare.replace('-', ''));
+    const [budgets] = await Promise.all([
+      runQuery(
+        q('zero_budgets')
+          .filter({
+            $and: [{ month: { $eq: budgetMonth } }],
+          })
+          .filter({
+            [conditionsOpKey]: filters.filter(filter => filter.category),
+          })
+          .groupBy([{ $id: '$category' }])
+          .select([
+            { category: { $id: '$category' } },
+            { amount: { $sum: '$amount' } },
+          ]),
+      ).then(({ data }) => data),
+    ]);
+
+    const dailyBudget =
+      budgets &&
+      integerToAmount(budgets.reduce((a, v) => (a = a + v.amount), 0)) /
+        compareInterval.length;
+
     const intervals = monthUtils.dayRangeInclusive(startDate, endDate);
-    intervals.push(
-      ...monthUtils.dayRangeInclusive(lastYearStartDate, lastYearEndDate),
-    );
+    if (endDateTo < startDate || startDateTo > endDate) {
+      intervals.push(...monthUtils.dayRangeInclusive(startDateTo, endDateTo));
+    }
+
     const days = [...Array(29).keys()]
       .filter(f => f > 0)
       .map(n => n.toString().padStart(2, '0'));
 
     let totalAssets = 0;
     let totalDebts = 0;
+    let totalBudget = 0;
 
-    const months = monthUtils
-      .rangeInclusive(startDate, monthUtils.currentMonth() + '-01')
-      .map(month => {
-        return { month, perMonthAssets: 0, perMonthDebts: 0 };
-      });
-
-    months.unshift({
-      month: monthUtils.prevYear(
-        monthUtils.subMonths(monthUtils.currentMonth(), 1),
-      ),
-      perMonthAssets: 0,
-      perMonthDebts: 0,
+    const months = monthUtils.rangeInclusive(startDate, endDate).map(month => {
+      return { month, perMonthAssets: 0, perMonthDebts: 0 };
     });
 
-    months.unshift({
-      month: monthUtils.prevYear(monthUtils.currentMonth()),
-      perMonthAssets: 0,
-      perMonthDebts: 0,
-    });
+    if (endDateTo < startDate || startDateTo > endDate) {
+      months.unshift({
+        month: compareTo,
+        perMonthAssets: 0,
+        perMonthDebts: 0,
+      });
+    }
 
     const intervalData = days.map(day => {
       let averageSum = 0;
@@ -124,13 +167,13 @@ export function createSpendingSpreadsheet({
             month.month === monthUtils.getMonth(intervalItem) &&
             day === offsetDay
           ) {
-            const intervalAssets = assets
+            const intervalAssets = combineAssets
               .filter(e => !e.categoryIncome && !e.accountOffBudget)
               .filter(asset => asset.date === intervalItem)
               .reduce((a, v) => (a = a + v.amount), 0);
             perIntervalAssets += intervalAssets;
 
-            const intervalDebts = debts
+            const intervalDebts = combineDebts
               .filter(e => !e.categoryIncome && !e.accountOffBudget)
               .filter(debt => debt.date === intervalItem)
               .reduce((a, v) => (a = a + v.amount), 0);
@@ -142,6 +185,10 @@ export function createSpendingSpreadsheet({
             let cumulativeAssets = 0;
             let cumulativeDebts = 0;
 
+            if (month.month === compare) {
+              totalBudget -= dailyBudget;
+            }
+
             months.map(m => {
               if (m.month === month.month) {
                 cumulativeAssets = m.perMonthAssets += perIntervalAssets;
@@ -149,15 +196,7 @@ export function createSpendingSpreadsheet({
               }
               return null;
             });
-            if (
-              month.month !== monthUtils.currentMonth() &&
-              month.month !== thisMonth &&
-              month.month !== monthUtils.prevYear(monthUtils.currentMonth()) &&
-              month.month !==
-                monthUtils.prevYear(
-                  monthUtils.subMonths(monthUtils.currentMonth(), 1),
-                )
-            ) {
+            if (month.month >= startDate && month.month < compare) {
               if (day === '28') {
                 if (monthUtils.getMonthEnd(intervalItem) === intervalItem) {
                   averageSum += cumulativeAssets + cumulativeDebts;
@@ -203,11 +242,9 @@ export function createSpendingSpreadsheet({
         months: indexedData,
         day,
         average: integerToAmount(averageSum) / monthCount,
-        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,
+        compare: dayData.filter(c => c.month === compare)[0].cumulative,
+        compareTo: dayData.filter(c => c.month === compareTo)[0].cumulative,
+        budget: totalBudget,
       };
     });
 
diff --git a/packages/loot-core/src/types/models/reports.d.ts b/packages/loot-core/src/types/models/reports.d.ts
index db9a798f1..465352a8e 100644
--- a/packages/loot-core/src/types/models/reports.d.ts
+++ b/packages/loot-core/src/types/models/reports.d.ts
@@ -30,13 +30,7 @@ export type balanceTypeOpType =
   | 'netAssets'
   | 'netDebts';
 
-export type spendingReportTimeType =
-  | 'average'
-  | 'thisMonth'
-  | 'lastMonth'
-  | 'twoMonthsPrevious'
-  | 'lastYear'
-  | 'lastYearPrevious';
+export type spendingReportModeType = 'singleMonth' | 'average' | 'budget';
 
 export type SpendingMonthEntity = Record<
   string | number,
@@ -61,11 +55,9 @@ export interface SpendingEntity {
     months: SpendingMonthEntity;
     day: string;
     average: number;
-    thisMonth: number;
-    lastMonth: number;
-    twoMonthsPrevious: number;
-    lastYear: number;
-    lastYearPrevious: number;
+    compare: number;
+    compareTo: number;
+    budget: 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 132fde3a9..b500745a7 100644
--- a/packages/loot-core/src/types/prefs.d.ts
+++ b/packages/loot-core/src/types/prefs.d.ts
@@ -1,4 +1,4 @@
-import { spendingReportTimeType } from './models/reports';
+import { spendingReportModeType } from './models/reports';
 
 export type FeatureFlag =
   | 'dashboards'
@@ -72,8 +72,9 @@ export type LocalPrefs = SyncedPrefs &
     reportsViewSummary: boolean;
     reportsViewLabel: boolean;
     spendingReportFilter: string;
-    spendingReportTime: spendingReportTimeType;
-    spendingReportCompare: spendingReportTimeType;
+    spendingReportMode: spendingReportModeType;
+    spendingReportCompare: string;
+    spendingReportCompareTo: string;
     sidebarWidth: number;
     'mobile.showSpentColumn': boolean;
   }>;
diff --git a/upcoming-release-notes/3380.md b/upcoming-release-notes/3380.md
new file mode 100644
index 000000000..d1d25f0e9
--- /dev/null
+++ b/upcoming-release-notes/3380.md
@@ -0,0 +1,6 @@
+---
+category: Enhancements
+authors: [carkom]
+---
+
+Fixing spending report header so that any month can be compared to any other month. Also adds budget as an optional comparison.
-- 
GitLab