From 933ca3ecca1455dec6a24a3e0f03e3016cc0d3cf Mon Sep 17 00:00:00 2001
From: Matiss Janis Aboltins <matiss@mja.lv>
Date: Fri, 5 May 2023 19:54:23 +0100
Subject: [PATCH] :sparkles: (reports) ability to fine-tune reports with
 filters (#994)

---
 .../src/components/reports/CashFlow.js        | 34 ++++++++++++--
 .../src/components/reports/Header.js          | 44 ++++++++-----------
 .../src/components/reports/NetWorth.js        | 32 +++++++++++++-
 .../reports/graphs/cash-flow-spreadsheet.js   |  9 +++-
 .../reports/graphs/net-worth-spreadsheet.js   | 19 +++++++-
 .../desktop-client/src/hooks/useFilters.ts    | 38 ++++++++++++++++
 upcoming-release-notes/994.md                 |  6 +++
 7 files changed, 148 insertions(+), 34 deletions(-)
 create mode 100644 packages/desktop-client/src/hooks/useFilters.ts
 create mode 100644 upcoming-release-notes/994.md

diff --git a/packages/desktop-client/src/components/reports/CashFlow.js b/packages/desktop-client/src/components/reports/CashFlow.js
index e93138708..c33a37317 100644
--- a/packages/desktop-client/src/components/reports/CashFlow.js
+++ b/packages/desktop-client/src/components/reports/CashFlow.js
@@ -6,7 +6,9 @@ import { send } from 'loot-core/src/platform/client/fetch';
 import * as monthUtils from 'loot-core/src/shared/months';
 import { integerToCurrency } from 'loot-core/src/shared/util';
 
+import useFilters from '../../hooks/useFilters';
 import { colors, styles } from '../../style';
+import { FilterButton, AppliedFilters } from '../accounts/Filters';
 import { View, Text, Block, P, AlignedText } from '../common';
 
 import Change from './Change';
@@ -16,6 +18,13 @@ import Header from './Header';
 import useReport from './useReport';
 
 function CashFlow() {
+  const {
+    filters,
+    onApply: onApplyFilter,
+    onDelete: onDeleteFilter,
+    onUpdate: onUpdateFilter,
+  } = useFilters();
+
   const [allMonths, setAllMonths] = useState(null);
   const [start, setStart] = useState(
     monthUtils.subMonths(monthUtils.currentMonth(), 30),
@@ -31,8 +40,8 @@ function CashFlow() {
   });
 
   const params = useMemo(
-    () => cashFlowByDate(start, end, isConcise),
-    [start, end, isConcise],
+    () => cashFlowByDate(start, end, isConcise, filters),
+    [start, end, isConcise, filters],
   );
   const data = useReport('cash_flow', params);
 
@@ -86,9 +95,28 @@ function CashFlow() {
         allMonths={allMonths}
         start={monthUtils.getMonth(start)}
         end={monthUtils.getMonth(end)}
-        show1Month={true}
+        show1Month
         onChangeDates={onChangeDates}
+        extraButtons={<FilterButton onApply={onApplyFilter} />}
       />
+
+      <View
+        style={{
+          marginTop: -10,
+          paddingLeft: 20,
+          paddingRight: 20,
+          backgroundColor: 'white',
+        }}
+      >
+        {filters.length > 0 && (
+          <AppliedFilters
+            filters={filters}
+            onUpdate={onUpdateFilter}
+            onDelete={onDeleteFilter}
+          />
+        )}
+      </View>
+
       <View
         style={{
           backgroundColor: 'white',
diff --git a/packages/desktop-client/src/components/reports/Header.js b/packages/desktop-client/src/components/reports/Header.js
index 855c560d0..91355d584 100644
--- a/packages/desktop-client/src/components/reports/Header.js
+++ b/packages/desktop-client/src/components/reports/Header.js
@@ -45,7 +45,15 @@ function getFullRange(allMonths) {
   return [start, end];
 }
 
-function Header({ title, start, end, show1Month, allMonths, onChangeDates }) {
+function Header({
+  title,
+  start,
+  end,
+  show1Month,
+  allMonths,
+  onChangeDates,
+  extraButtons,
+}) {
   return (
     <View
       style={{
@@ -68,6 +76,7 @@ function Header({ title, start, end, show1Month, allMonths, onChangeDates }) {
           flexDirection: 'row',
           alignItems: 'center',
           marginTop: 15,
+          gap: 15,
         }}
       >
         <div>
@@ -99,41 +108,24 @@ function Header({ title, start, end, show1Month, allMonths, onChangeDates }) {
             ))}
           </Select>
         </div>
+
+        {extraButtons}
+
         {show1Month && (
-          <Button
-            bare
-            style={{ marginLeft: 15 }}
-            onClick={() => onChangeDates(...getLatestRange(1))}
-          >
+          <Button bare onClick={() => onChangeDates(...getLatestRange(1))}>
             1 month
           </Button>
         )}
-        <Button
-          bare
-          style={{ marginLeft: 15 }}
-          onClick={() => onChangeDates(...getLatestRange(2))}
-        >
+        <Button bare onClick={() => onChangeDates(...getLatestRange(2))}>
           3 months
         </Button>
-        <Button
-          bare
-          style={{ marginLeft: 15 }}
-          onClick={() => onChangeDates(...getLatestRange(5))}
-        >
+        <Button bare onClick={() => onChangeDates(...getLatestRange(5))}>
           6 months
         </Button>
-        <Button
-          bare
-          style={{ marginLeft: 15 }}
-          onClick={() => onChangeDates(...getLatestRange(12))}
-        >
+        <Button bare onClick={() => onChangeDates(...getLatestRange(12))}>
           1 Year
         </Button>
-        <Button
-          bare
-          style={{ marginLeft: 15 }}
-          onClick={() => onChangeDates(...getFullRange(allMonths))}
-        >
+        <Button bare onClick={() => onChangeDates(...getFullRange(allMonths))}>
           All Time
         </Button>
       </View>
diff --git a/packages/desktop-client/src/components/reports/NetWorth.js b/packages/desktop-client/src/components/reports/NetWorth.js
index a7f36e44e..fc3f314e0 100644
--- a/packages/desktop-client/src/components/reports/NetWorth.js
+++ b/packages/desktop-client/src/components/reports/NetWorth.js
@@ -9,7 +9,9 @@ import { send } from 'loot-core/src/platform/client/fetch';
 import * as monthUtils from 'loot-core/src/shared/months';
 import { integerToCurrency } from 'loot-core/src/shared/util';
 
+import useFilters from '../../hooks/useFilters';
 import { styles } from '../../style';
+import { FilterButton, AppliedFilters } from '../accounts/Filters';
 import { View, P } from '../common';
 
 import Change from './Change';
@@ -20,6 +22,13 @@ import useReport from './useReport';
 import { fromDateRepr } from './util';
 
 function NetWorth({ accounts }) {
+  const {
+    filters,
+    onApply: onApplyFilter,
+    onDelete: onDeleteFilter,
+    onUpdate: onUpdateFilter,
+  } = useFilters();
+
   const [allMonths, setAllMonths] = useState(null);
   const [start, setStart] = useState(
     monthUtils.subMonths(monthUtils.currentMonth(), 5),
@@ -27,8 +36,8 @@ function NetWorth({ accounts }) {
   const [end, setEnd] = useState(monthUtils.currentMonth());
 
   const params = useMemo(
-    () => netWorthSpreadsheet(start, end, accounts),
-    [start, end, accounts],
+    () => netWorthSpreadsheet(start, end, accounts, filters),
+    [start, end, accounts, filters],
   );
   const data = useReport('net_worth', params);
 
@@ -78,7 +87,26 @@ function NetWorth({ accounts }) {
         start={start}
         end={end}
         onChangeDates={onChangeDates}
+        extraButtons={<FilterButton onApply={onApplyFilter} />}
       />
+
+      <View
+        style={{
+          marginTop: -10,
+          paddingLeft: 20,
+          paddingRight: 20,
+          backgroundColor: 'white',
+        }}
+      >
+        {filters.length > 0 && (
+          <AppliedFilters
+            filters={filters}
+            onUpdate={onUpdateFilter}
+            onDelete={onDeleteFilter}
+          />
+        )}
+      </View>
+
       <View
         style={{
           backgroundColor: 'white',
diff --git a/packages/desktop-client/src/components/reports/graphs/cash-flow-spreadsheet.js b/packages/desktop-client/src/components/reports/graphs/cash-flow-spreadsheet.js
index 0605ceadc..4ac4af574 100644
--- a/packages/desktop-client/src/components/reports/graphs/cash-flow-spreadsheet.js
+++ b/packages/desktop-client/src/components/reports/graphs/cash-flow-spreadsheet.js
@@ -3,6 +3,7 @@ import React from 'react';
 import * as d from 'date-fns';
 
 import q from 'loot-core/src/client/query-helpers';
+import { send } from 'loot-core/src/platform/client/fetch';
 import * as monthUtils from 'loot-core/src/shared/months';
 import { integerToCurrency, integerToAmount } from 'loot-core/src/shared/util';
 
@@ -43,11 +44,16 @@ export function simpleCashFlow(start, end) {
   };
 }
 
-export function cashFlowByDate(start, end, isConcise) {
+export function cashFlowByDate(start, end, isConcise, conditions = []) {
   return async (spreadsheet, setData) => {
+    let { filters } = await send('make-filters-from-conditions', {
+      conditions: conditions.filter(cond => !cond.customName),
+    });
+
     function makeQuery(where) {
       let query = q('transactions').filter({
         $and: [
+          ...filters,
           { date: { $transform: '$month', $gte: start } },
           { date: { $transform: '$month', $lte: end } },
         ],
@@ -78,6 +84,7 @@ export function cashFlowByDate(start, end, isConcise) {
       [
         q('transactions')
           .filter({
+            $and: filters,
             date: { $transform: '$month', $lt: start },
             'account.offbudget': false,
           })
diff --git a/packages/desktop-client/src/components/reports/graphs/net-worth-spreadsheet.js b/packages/desktop-client/src/components/reports/graphs/net-worth-spreadsheet.js
index cda519f54..99bf6ce1e 100644
--- a/packages/desktop-client/src/components/reports/graphs/net-worth-spreadsheet.js
+++ b/packages/desktop-client/src/components/reports/graphs/net-worth-spreadsheet.js
@@ -3,6 +3,7 @@ import React from 'react';
 import * as d from 'date-fns';
 
 import q, { runQuery } from 'loot-core/src/client/query-helpers';
+import { send } from 'loot-core/src/platform/client/fetch';
 import * as monthUtils from 'loot-core/src/shared/months';
 import {
   integerToCurrency,
@@ -13,18 +14,31 @@ import {
 import { AlignedText } from '../../common';
 import { index } from '../util';
 
-export default function createSpreadsheet(start, end, accounts) {
+export default function createSpreadsheet(
+  start,
+  end,
+  accounts,
+  conditions = [],
+) {
   return async (spreadsheet, setData) => {
     if (accounts.length === 0) {
       return null;
     }
 
+    let { filters } = await send('make-filters-from-conditions', {
+      conditions: conditions.filter(cond => !cond.customName),
+    });
+
     const data = await Promise.all(
       accounts.map(async acct => {
         let [starting, balances] = await Promise.all([
           runQuery(
             q('transactions')
-              .filter({ account: acct.id, date: { $lt: start + '-01' } })
+              .filter({
+                $and: filters,
+                account: acct.id,
+                date: { $lt: start + '-01' },
+              })
               .calculate({ $sum: '$amount' }),
           ).then(({ data }) => data),
 
@@ -33,6 +47,7 @@ export default function createSpreadsheet(start, end, accounts) {
               .filter({
                 account: acct.id,
                 $and: [
+                  ...filters,
                   { date: { $gte: start + '-01' } },
                   { date: { $lte: end + '-31' } },
                 ],
diff --git a/packages/desktop-client/src/hooks/useFilters.ts b/packages/desktop-client/src/hooks/useFilters.ts
new file mode 100644
index 000000000..bb13ab0b3
--- /dev/null
+++ b/packages/desktop-client/src/hooks/useFilters.ts
@@ -0,0 +1,38 @@
+import { useCallback, useMemo, useState } from 'react';
+
+export default function useFilters<T>(initialFilters: T[] = []) {
+  const [filters, setFilters] = useState<T[]>(initialFilters);
+
+  const onApply = useCallback(
+    (newFilter: T) => {
+      setFilters(state => [...state, newFilter]);
+    },
+    [setFilters],
+  );
+
+  const onUpdate = useCallback(
+    (oldFilter: T, updatedFilter: T) => {
+      setFilters(state =>
+        state.map(f => (f === oldFilter ? updatedFilter : f)),
+      );
+    },
+    [setFilters],
+  );
+
+  const onDelete = useCallback(
+    (deletedFilter: T) => {
+      setFilters(state => state.filter(f => f !== deletedFilter));
+    },
+    [setFilters],
+  );
+
+  return useMemo(
+    () => ({
+      filters,
+      onApply,
+      onUpdate,
+      onDelete,
+    }),
+    [filters, onApply, onUpdate, onDelete],
+  );
+}
diff --git a/upcoming-release-notes/994.md b/upcoming-release-notes/994.md
new file mode 100644
index 000000000..d89cb755b
--- /dev/null
+++ b/upcoming-release-notes/994.md
@@ -0,0 +1,6 @@
+---
+category: Enhancements
+authors: [MatissJanis]
+---
+
+Reports: ability to filter the data by payee/account/category/etc.
-- 
GitLab