From 1ee2cbec1c8a10441d9a652e27dc56bd03c9f4ea Mon Sep 17 00:00:00 2001
From: Neil <55785687+carkom@users.noreply.github.com>
Date: Tue, 9 Apr 2024 18:37:07 +0100
Subject: [PATCH] Custom Reports: Show Activity onClick (#2522)

* barGraph

* notes

* sessionStorage

* sessionStorage Complete

* Click for details

* mode click graphType

* compact

* hidden and offbudget working

* add pointer

* typecheck fix
---
 .../src/components/reports/ChooseGraph.tsx    |  6 ++
 .../src/components/reports/Overview.jsx       |  4 +
 .../src/components/reports/ReportSidebar.jsx  | 71 ++++++++++++++++
 .../src/components/reports/ReportTopbar.jsx   | 29 +++----
 .../components/reports/graphs/BarGraph.tsx    | 84 ++++++++++++++++++-
 .../reports/reports/CustomReport.jsx          | 26 +++++-
 upcoming-release-notes/2522.md                |  6 ++
 7 files changed, 208 insertions(+), 18 deletions(-)
 create mode 100644 upcoming-release-notes/2522.md

diff --git a/packages/desktop-client/src/components/reports/ChooseGraph.tsx b/packages/desktop-client/src/components/reports/ChooseGraph.tsx
index 1e47c0c61..e68b99ef3 100644
--- a/packages/desktop-client/src/components/reports/ChooseGraph.tsx
+++ b/packages/desktop-client/src/components/reports/ChooseGraph.tsx
@@ -32,6 +32,8 @@ type ChooseGraphProps = {
   viewLabels?: boolean;
   compact?: boolean;
   style?: CSSProperties;
+  showHiddenCategories?: boolean;
+  showOffBudget?: boolean;
 };
 
 export function ChooseGraph({
@@ -47,6 +49,8 @@ export function ChooseGraph({
   viewLabels,
   compact,
   style,
+  showHiddenCategories,
+  showOffBudget,
 }: ChooseGraphProps) {
   const intervals: string[] = monthUtils.rangeInclusive(startDate, endDate);
   const graphStyle = compact ? { ...style } : { flexGrow: 1 };
@@ -104,6 +108,8 @@ export function ChooseGraph({
         groupBy={groupBy}
         balanceTypeOp={balanceTypeOp}
         viewLabels={viewLabels}
+        showHiddenCategories={showHiddenCategories}
+        showOffBudget={showOffBudget}
       />
     );
   }
diff --git a/packages/desktop-client/src/components/reports/Overview.jsx b/packages/desktop-client/src/components/reports/Overview.jsx
index 939421c30..e6a5e00d6 100644
--- a/packages/desktop-client/src/components/reports/Overview.jsx
+++ b/packages/desktop-client/src/components/reports/Overview.jsx
@@ -1,4 +1,5 @@
 import React from 'react';
+import { useLocation } from 'react-router-dom';
 
 import { useReports } from 'loot-core/src/client/data-hooks/reports';
 
@@ -17,6 +18,9 @@ import { NetWorthCard } from './reports/NetWorthCard';
 export function Overview() {
   const customReports = useReports();
 
+  const location = useLocation();
+  sessionStorage.setItem('url', location.pathname);
+
   const customReportsFeatureFlag = useFeatureFlag('customReports');
 
   const accounts = useAccounts();
diff --git a/packages/desktop-client/src/components/reports/ReportSidebar.jsx b/packages/desktop-client/src/components/reports/ReportSidebar.jsx
index 3b2585eba..b537911bf 100644
--- a/packages/desktop-client/src/components/reports/ReportSidebar.jsx
+++ b/packages/desktop-client/src/components/reports/ReportSidebar.jsx
@@ -42,22 +42,40 @@ export function ReportSidebar({
 }) {
   const [menuOpen, setMenuOpen] = useState(false);
   const onSelectRange = cond => {
+    const storedReport = JSON.parse(sessionStorage.getItem('report'));
+    sessionStorage.setItem(
+      'report',
+      JSON.stringify({ ...storedReport, dateRange: cond }),
+    );
     onReportChange({ type: 'modify' });
     setDateRange(cond);
     onChangeDates(...getLiveRange(cond, earliestTransaction));
   };
 
   const onChangeMode = cond => {
+    const storedReport = JSON.parse(sessionStorage.getItem('report'));
+    sessionStorage.setItem(
+      'report',
+      JSON.stringify({ ...storedReport, mode: cond }),
+    );
     onReportChange({ type: 'modify' });
     setMode(cond);
     let graph;
     if (cond === 'time') {
       if (customReportItems.graphType === 'BarGraph') {
+        sessionStorage.setItem(
+          'report',
+          JSON.stringify({ ...storedReport, graphType: 'StackedBarGraph' }),
+        );
         setGraphType('StackedBarGraph');
         graph = 'StackedBarGraph';
       }
     } else {
       if (customReportItems.graphType === 'StackedBarGraph') {
+        sessionStorage.setItem(
+          'report',
+          JSON.stringify({ ...storedReport, graphType: 'BarGraph' }),
+        );
         setGraphType('BarGraph');
         graph = 'BarGraph';
       }
@@ -66,12 +84,22 @@ export function ReportSidebar({
   };
 
   const onChangeSplit = cond => {
+    const storedReport = JSON.parse(sessionStorage.getItem('report'));
+    sessionStorage.setItem(
+      'report',
+      JSON.stringify({ ...storedReport, groupBy: cond }),
+    );
     onReportChange({ type: 'modify' });
     setGroupBy(cond);
     defaultItems(cond);
   };
 
   const onChangeBalanceType = cond => {
+    const storedReport = JSON.parse(sessionStorage.getItem('report'));
+    sessionStorage.setItem(
+      'report',
+      JSON.stringify({ ...storedReport, balanceType: cond }),
+    );
     onReportChange({ type: 'modify' });
     setBalanceType(cond);
   };
@@ -217,17 +245,50 @@ export function ReportSidebar({
               >
                 <Menu
                   onMenuSelect={type => {
+                    const storedReport = JSON.parse(
+                      sessionStorage.getItem('report'),
+                    );
                     onReportChange({ type: 'modify' });
 
                     if (type === 'show-hidden-categories') {
+                      sessionStorage.setItem(
+                        'report',
+                        JSON.stringify({
+                          ...storedReport,
+                          showHiddenCategories:
+                            !customReportItems.showHiddenCategories,
+                        }),
+                      );
                       setShowHiddenCategories(
                         !customReportItems.showHiddenCategories,
                       );
                     } else if (type === 'show-off-budget') {
+                      sessionStorage.setItem(
+                        'report',
+                        JSON.stringify({
+                          ...storedReport,
+                          showOffBudget: !customReportItems.showOffBudget,
+                        }),
+                      );
                       setShowOffBudget(!customReportItems.showOffBudget);
                     } else if (type === 'show-empty-items') {
+                      sessionStorage.setItem(
+                        'report',
+                        JSON.stringify({
+                          ...storedReport,
+                          showEmpty: !customReportItems.showEmpty,
+                        }),
+                      );
                       setShowEmpty(!customReportItems.showEmpty);
                     } else if (type === 'show-uncategorized') {
+                      sessionStorage.setItem(
+                        'report',
+                        JSON.stringify({
+                          ...storedReport,
+                          showUncategorized:
+                            !customReportItems.showUncategorized,
+                        }),
+                      );
                       setShowUncategorized(
                         !customReportItems.showUncategorized,
                       );
@@ -287,6 +348,11 @@ export function ReportSidebar({
           <ModeButton
             selected={!customReportItems.isDateStatic}
             onSelect={() => {
+              const storedReport = JSON.parse(sessionStorage.getItem('report'));
+              sessionStorage.setItem(
+                'report',
+                JSON.stringify({ ...storedReport, isDateStatic: false }),
+              );
               setIsDateStatic(false);
               onSelectRange(customReportItems.dateRange);
             }}
@@ -296,6 +362,11 @@ export function ReportSidebar({
           <ModeButton
             selected={customReportItems.isDateStatic}
             onSelect={() => {
+              const storedReport = JSON.parse(sessionStorage.getItem('report'));
+              sessionStorage.setItem(
+                'report',
+                JSON.stringify({ ...storedReport, isDateStatic: true }),
+              );
               setIsDateStatic(true);
               onChangeDates(
                 customReportItems.startDate,
diff --git a/packages/desktop-client/src/components/reports/ReportTopbar.jsx b/packages/desktop-client/src/components/reports/ReportTopbar.jsx
index ab23885b0..3a98f00ee 100644
--- a/packages/desktop-client/src/components/reports/ReportTopbar.jsx
+++ b/packages/desktop-client/src/components/reports/ReportTopbar.jsx
@@ -31,6 +31,17 @@ export function ReportTopbar({
   disabledItems,
   defaultItems,
 }) {
+  const onChangeGraph = cond => {
+    const storedReport = JSON.parse(sessionStorage.getItem('report'));
+    sessionStorage.setItem(
+      'report',
+      JSON.stringify({ ...storedReport, graphType: cond }),
+    );
+    onReportChange({ type: 'modify' });
+    setGraphType(cond);
+    defaultItems(cond);
+  };
+
   return (
     <View
       style={{
@@ -44,9 +55,7 @@ export function ReportTopbar({
         selected={customReportItems.graphType === 'TableGraph'}
         title="Data Table"
         onSelect={() => {
-          onReportChange({ type: 'modify' });
-          setGraphType('TableGraph');
-          defaultItems('TableGraph');
+          onChangeGraph('TableGraph');
         }}
         style={{ marginRight: 15 }}
         disabled={disabledItems('TableGraph')}
@@ -62,11 +71,7 @@ export function ReportTopbar({
           customReportItems.graphType === 'StackedBarGraph'
         }
         onSelect={() => {
-          onReportChange({ type: 'modify' });
-          setGraphType(
-            customReportItems.mode === 'total' ? 'BarGraph' : 'StackedBarGraph',
-          );
-          defaultItems(
+          onChangeGraph(
             customReportItems.mode === 'total' ? 'BarGraph' : 'StackedBarGraph',
           );
         }}
@@ -94,9 +99,7 @@ export function ReportTopbar({
         title="Area Graph"
         selected={customReportItems.graphType === 'AreaGraph'}
         onSelect={() => {
-          onReportChange({ type: 'modify' });
-          setGraphType('AreaGraph');
-          defaultItems('AreaGraph');
+          onChangeGraph('AreaGraph');
         }}
         style={{ marginRight: 15 }}
         disabled={disabledItems('AreaGraph')}
@@ -107,9 +110,7 @@ export function ReportTopbar({
         title="Donut Graph"
         selected={customReportItems.graphType === 'DonutGraph'}
         onSelect={() => {
-          onReportChange({ type: 'modify' });
-          setGraphType('DonutGraph');
-          defaultItems('DonutGraph');
+          onChangeGraph('DonutGraph');
         }}
         style={{ marginRight: 15 }}
         disabled={disabledItems('DonutGraph')}
diff --git a/packages/desktop-client/src/components/reports/graphs/BarGraph.tsx b/packages/desktop-client/src/components/reports/graphs/BarGraph.tsx
index f85572d8c..e9d5252b4 100644
--- a/packages/desktop-client/src/components/reports/graphs/BarGraph.tsx
+++ b/packages/desktop-client/src/components/reports/graphs/BarGraph.tsx
@@ -1,5 +1,5 @@
 // @ts-strict-ignore
-import React from 'react';
+import React, { useState } from 'react';
 
 import { css } from 'glamor';
 import {
@@ -21,9 +21,12 @@ import {
 } from 'loot-core/src/shared/util';
 import { type GroupedEntity } from 'loot-core/src/types/models/reports';
 
+import { useAccounts } from '../../../hooks/useAccounts';
+import { useCategories } from '../../../hooks/useCategories';
+import { useNavigate } from '../../../hooks/useNavigate';
 import { usePrivacyMode } from '../../../hooks/usePrivacyMode';
-import { theme } from '../../../style';
 import { type CSSProperties } from '../../../style';
+import { theme } from '../../../style/index';
 import { AlignedText } from '../../common/AlignedText';
 import { Container } from '../Container';
 import { getCustomTick } from '../getCustomTick';
@@ -133,6 +136,8 @@ type BarGraphProps = {
   balanceTypeOp: string;
   compact?: boolean;
   viewLabels: boolean;
+  showHiddenCategories?: boolean;
+  showOffBudget?: boolean;
 };
 
 export function BarGraph({
@@ -142,8 +147,14 @@ export function BarGraph({
   balanceTypeOp,
   compact,
   viewLabels,
+  showHiddenCategories,
+  showOffBudget,
 }: BarGraphProps) {
+  const navigate = useNavigate();
+  const categories = useCategories();
+  const accounts = useAccounts();
   const privacyMode = usePrivacyMode();
+  const [pointer, setPointer] = useState('');
 
   const yAxis = groupBy === 'Interval' ? 'date' : 'name';
   const splitData = groupBy === 'Interval' ? 'intervalData' : 'data';
@@ -166,6 +177,61 @@ export function BarGraph({
     .reduce((acc, cur) => (Math.abs(cur) > Math.abs(acc) ? cur : acc), 0);
 
   const leftMargin = Math.abs(largestValue) > 1000000 ? 20 : 0;
+
+  const onShowActivity = item => {
+    const amount = balanceTypeOp === 'totalDebts' ? 'lte' : 'gte';
+    const field = groupBy === 'Interval' ? null : groupBy.toLowerCase();
+    const hiddenCategories = categories.list
+      .filter(f => f.hidden)
+      .map(e => e.id);
+    const offBudgetAccounts = accounts.filter(f => f.offbudget).map(e => e.id);
+
+    const conditions = [
+      { field, op: 'is', value: item.id, type: 'id' },
+      {
+        field: 'date',
+        op: 'gte',
+        value: data.startDate,
+        options: { date: true },
+        type: 'date',
+      },
+      {
+        field: 'date',
+        op: 'lte',
+        value: data.endDate,
+        options: { date: true },
+        type: 'date',
+      },
+      balanceTypeOp !== 'totalTotals' && {
+        field: 'amount',
+        op: amount,
+        value: 0,
+        type: 'number',
+      },
+      hiddenCategories.length > 0 &&
+        !showHiddenCategories && {
+          field: 'category',
+          op: 'notOneOf',
+          value: hiddenCategories,
+          type: 'id',
+        },
+      offBudgetAccounts.length > 0 &&
+        !showOffBudget && {
+          field: 'account',
+          op: 'notOneOf',
+          value: offBudgetAccounts,
+          type: 'id',
+        },
+    ].filter(f => f);
+    navigate('/accounts', {
+      state: {
+        goBack: true,
+        conditions,
+        categoryId: item.id,
+      },
+    });
+  };
+
   return (
     <Container
       style={{
@@ -183,6 +249,7 @@ export function BarGraph({
                 height={height}
                 stackOffset="sign"
                 data={data[splitData]}
+                style={{ cursor: pointer }}
                 margin={{
                   top: labelsMargin,
                   right: 0,
@@ -226,7 +293,18 @@ export function BarGraph({
                     <ReferenceLine y={0} stroke={theme.pageTextLight} />
                   </>
                 )}
-                <Bar dataKey={val => getVal(val)} stackId="a">
+                <Bar
+                  dataKey={val => getVal(val)}
+                  stackId="a"
+                  onMouseLeave={() => setPointer('')}
+                  onMouseEnter={() =>
+                    !['Group', 'Interval'].includes(groupBy) &&
+                    setPointer('pointer')
+                  }
+                  onClick={
+                    !['Group', 'Interval'].includes(groupBy) && onShowActivity
+                  }
+                >
                   {viewLabels && !compact && (
                     <LabelList
                       dataKey={val => getVal(val)}
diff --git a/packages/desktop-client/src/components/reports/reports/CustomReport.jsx b/packages/desktop-client/src/components/reports/reports/CustomReport.jsx
index c6da3eb0a..db792e71e 100644
--- a/packages/desktop-client/src/components/reports/reports/CustomReport.jsx
+++ b/packages/desktop-client/src/components/reports/reports/CustomReport.jsx
@@ -54,9 +54,19 @@ export function CustomReport() {
   } = useFilters();
 
   const location = useLocation();
-  const loadReport = location.state
+
+  const prevUrl = sessionStorage.getItem('url');
+
+  sessionStorage.setItem('prevUrl', prevUrl);
+  sessionStorage.setItem('url', location.pathname);
+
+  if (['/reports'].includes(prevUrl)) sessionStorage.clear();
+
+  const session = JSON.parse(sessionStorage.getItem('report'));
+  const combine = location.state
     ? location.state.report ?? defaultReport
     : defaultReport;
+  const loadReport = { ...combine, ...session };
 
   const [allIntervals, setAllIntervals] = useState(null);
 
@@ -319,6 +329,15 @@ export function CustomReport() {
   };
 
   const onChangeDates = (dateStart, dateEnd) => {
+    const storedReport = JSON.parse(sessionStorage.getItem('report'));
+    sessionStorage.setItem(
+      'report',
+      JSON.stringify({
+        ...storedReport,
+        startDate: dateStart,
+        endDate: dateEnd,
+      }),
+    );
     setStartDate(dateStart);
     setEndDate(dateEnd);
     onReportChange({ type: 'modify' });
@@ -381,15 +400,18 @@ export function CustomReport() {
         }
         break;
       case 'reload':
+        sessionStorage.clear();
         setSavedStatus('saved');
         setReportData(report);
         break;
       case 'reset':
+        sessionStorage.clear();
         setSavedStatus('new');
         setReport(defaultReport);
         setReportData(defaultReport);
         break;
       case 'choose':
+        sessionStorage.clear();
         setSavedStatus('saved');
         setReport(savedReport);
         setReportData(savedReport);
@@ -554,6 +576,8 @@ export function CustomReport() {
                     setScrollWidth={setScrollWidth}
                     viewLabels={viewLabels}
                     compact={false}
+                    showHiddenCategories={showHiddenCategories}
+                    showOffBudget={showOffBudget}
                   />
                 ) : (
                   <LoadingIndicator message="Loading report..." />
diff --git a/upcoming-release-notes/2522.md b/upcoming-release-notes/2522.md
new file mode 100644
index 000000000..be098155e
--- /dev/null
+++ b/upcoming-release-notes/2522.md
@@ -0,0 +1,6 @@
+---
+category: Enhancements
+authors: [carkom]
+---
+
+Custom reports so transactions activity on accounts page for graphs when clicked.
-- 
GitLab