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