From 8a70d2464d20c2bebae4bc6778e82641f410561f Mon Sep 17 00:00:00 2001 From: Neil <55785687+carkom@users.noreply.github.com> Date: Tue, 7 May 2024 18:02:17 +0100 Subject: [PATCH] Custom Reports: Show table activity (#2696) * Show table activity * notes * lint fix * add underline * add linkstyle * fix table aligment --- .../budget/BalanceWithCarryover.tsx | 6 +- .../src/components/reports/ChooseGraph.tsx | 15 ++- .../components/reports/graphs/showActivity.ts | 87 ++++++++++++ .../reports/graphs/tableGraph/ReportTable.tsx | 28 ++-- .../graphs/tableGraph/ReportTableList.tsx | 21 +-- .../graphs/tableGraph/ReportTableRow.tsx | 126 +++++++++++++++++- .../graphs/tableGraph/ReportTableTotals.tsx | 104 ++++++++++++++- .../desktop-client/src/components/table.tsx | 12 +- .../loot-core/src/types/models/reports.d.ts | 2 + upcoming-release-notes/2696.md | 6 + 10 files changed, 369 insertions(+), 38 deletions(-) create mode 100644 packages/desktop-client/src/components/reports/graphs/showActivity.ts create mode 100644 upcoming-release-notes/2696.md diff --git a/packages/desktop-client/src/components/budget/BalanceWithCarryover.tsx b/packages/desktop-client/src/components/budget/BalanceWithCarryover.tsx index bc3275e44..2db27cbe1 100644 --- a/packages/desktop-client/src/components/budget/BalanceWithCarryover.tsx +++ b/packages/desktop-client/src/components/budget/BalanceWithCarryover.tsx @@ -16,7 +16,6 @@ type BalanceWithCarryoverProps = { goal?: ComponentProps<typeof CellValue>['binding']; budgeted?: ComponentProps<typeof CellValue>['binding']; disabled?: boolean; - style?: CSSProperties; balanceStyle?: CSSProperties; carryoverStyle?: CSSProperties; }; @@ -26,7 +25,6 @@ export function BalanceWithCarryover({ goal, budgeted, disabled, - style, balanceStyle, carryoverStyle, }: BalanceWithCarryoverProps) { @@ -36,7 +34,7 @@ export function BalanceWithCarryover({ const budgetedValue = useSheetValue(budgeted); const isGoalTemplatesEnabled = useFeatureFlag('goalTemplatesEnabled'); return ( - <View style={style}> + <> <CellValue binding={balance} type="financial" @@ -80,6 +78,6 @@ export function BalanceWithCarryover({ /> </View> )} - </View> + </> ); } diff --git a/packages/desktop-client/src/components/reports/ChooseGraph.tsx b/packages/desktop-client/src/components/reports/ChooseGraph.tsx index a68045360..c8afbdd50 100644 --- a/packages/desktop-client/src/components/reports/ChooseGraph.tsx +++ b/packages/desktop-client/src/components/reports/ChooseGraph.tsx @@ -54,12 +54,6 @@ export function ChooseGraph({ }: ChooseGraphProps) { const graphStyle = compact ? { ...style } : { flexGrow: 1 }; const balanceTypeOp = ReportOptions.balanceTypeMap.get(balanceType); - const groupByData = - groupBy === 'Category' - ? 'groupedData' - : groupBy === 'Interval' - ? 'intervalData' - : 'data'; const saveScrollWidth = value => { setScrollWidth(!value ? 0 : value); @@ -180,12 +174,15 @@ export function ChooseGraph({ handleScroll={handleScroll} balanceTypeOp={balanceTypeOp} groupBy={groupBy} - data={data[groupByData]} + data={data} + filters={filters} mode={mode} intervalsCount={intervalsCount} compact={compact} style={rowStyle} compactStyle={compactStyle} + showHiddenCategories={showHiddenCategories} + showOffBudget={showOffBudget} /> <ReportTableTotals totalScrollRef={totalScrollRef} @@ -197,6 +194,10 @@ export function ChooseGraph({ compact={compact} style={rowStyle} compactStyle={compactStyle} + groupBy={groupBy} + filters={filters} + showHiddenCategories={showHiddenCategories} + showOffBudget={showOffBudget} /> </View> ); diff --git a/packages/desktop-client/src/components/reports/graphs/showActivity.ts b/packages/desktop-client/src/components/reports/graphs/showActivity.ts new file mode 100644 index 000000000..f0f6673bd --- /dev/null +++ b/packages/desktop-client/src/components/reports/graphs/showActivity.ts @@ -0,0 +1,87 @@ +import { type NavigateFunction } from 'react-router-dom'; + +import { type AccountEntity } from 'loot-core/types/models/account'; +import { type CategoryEntity } from 'loot-core/types/models/category'; +import { type CategoryGroupEntity } from 'loot-core/types/models/category-group'; +import { type RuleConditionEntity } from 'loot-core/types/models/rule'; + +type showActivityProps = { + navigate: NavigateFunction; + categories: { list: CategoryEntity[]; grouped: CategoryGroupEntity[] }; + accounts: AccountEntity[]; + balanceTypeOp: string; + filters: RuleConditionEntity[]; + showHiddenCategories: boolean; + showOffBudget: boolean; + type: string; + startDate: string; + endDate?: string; + field?: string; + id?: string; +}; + +export function showActivity({ + navigate, + categories, + accounts, + balanceTypeOp, + filters, + showHiddenCategories, + showOffBudget, + type, + startDate, + endDate, + field, + id, +}: showActivityProps) { + const amount = + balanceTypeOp === 'totalDebts' || type === 'debts' ? 'lte' : 'gte'; + 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 = [ + ...filters, + id && { field, op: 'is', value: id, type: 'id' }, + { + field: 'date', + op: type === 'time' ? 'is' : 'gte', + value: startDate, + options: { date: true }, + }, + type !== 'time' && { + field: 'date', + op: 'lte', + value: endDate, + options: { date: true }, + }, + !( + balanceTypeOp === 'totalTotals' && + (type === 'totals' || type === 'time') + ) && { + 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, + }, + }); +} diff --git a/packages/desktop-client/src/components/reports/graphs/tableGraph/ReportTable.tsx b/packages/desktop-client/src/components/reports/graphs/tableGraph/ReportTable.tsx index 45939a4bc..9987321db 100644 --- a/packages/desktop-client/src/components/reports/graphs/tableGraph/ReportTable.tsx +++ b/packages/desktop-client/src/components/reports/graphs/tableGraph/ReportTable.tsx @@ -7,7 +7,8 @@ import React, { } from 'react'; import { type RefProp } from 'react-spring'; -import { type DataEntity } from 'loot-core/src/types/models/reports'; +import { type GroupedEntity } from 'loot-core/src/types/models/reports'; +import { type RuleConditionEntity } from 'loot-core/types/models/rule'; import { type CSSProperties } from '../../../../style'; import { Block } from '../../../common/Block'; @@ -22,12 +23,15 @@ type ReportTableProps = { handleScroll: UIEventHandler<HTMLDivElement>; groupBy: string; balanceTypeOp: 'totalDebts' | 'totalTotals' | 'totalAssets'; - data: DataEntity[]; + data: GroupedEntity; + filters?: RuleConditionEntity[]; mode: string; intervalsCount: number; compact: boolean; style?: CSSProperties; compactStyle?: CSSProperties; + showHiddenCategories?: boolean; + showOffBudget?: boolean; }; export function ReportTable({ @@ -37,11 +41,14 @@ export function ReportTable({ groupBy, balanceTypeOp, data, + filters, mode, intervalsCount, compact, style, compactStyle, + showHiddenCategories, + showOffBudget, }: ReportTableProps) { const contentRef = useRef<HTMLDivElement>(null); @@ -52,25 +59,22 @@ export function ReportTable({ }); const renderItem = useCallback( - ({ - item, - groupByItem, - mode, - intervalsCount, - compact, - style, - compactStyle, - }) => { + ({ item, mode, intervalsCount, compact, style, compactStyle }) => { return ( <ReportTableRow item={item} balanceTypeOp={balanceTypeOp} - groupByItem={groupByItem} + groupBy={groupBy} mode={mode} + filters={filters} + startDate={data.startDate} + endDate={data.endDate} intervalsCount={intervalsCount} compact={compact} style={style} compactStyle={compactStyle} + showHiddenCategories={showHiddenCategories} + showOffBudget={showOffBudget} /> ); }, diff --git a/packages/desktop-client/src/components/reports/graphs/tableGraph/ReportTableList.tsx b/packages/desktop-client/src/components/reports/graphs/tableGraph/ReportTableList.tsx index 1cdc0e160..e619d4e0b 100644 --- a/packages/desktop-client/src/components/reports/graphs/tableGraph/ReportTableList.tsx +++ b/packages/desktop-client/src/components/reports/graphs/tableGraph/ReportTableList.tsx @@ -1,14 +1,14 @@ // @ts-strict-ignore import React from 'react'; -import { type DataEntity } from 'loot-core/src/types/models/reports'; +import { type GroupedEntity } from 'loot-core/src/types/models/reports'; import { type CSSProperties, theme } from '../../../../style'; import { View } from '../../../common/View'; import { Row } from '../../../table'; type ReportTableListProps = { - data: DataEntity[]; + data: GroupedEntity; mode?: string; intervalsCount?: number; groupBy: string; @@ -28,7 +28,13 @@ export function ReportTableList({ style, compactStyle, }: ReportTableListProps) { - const groupByItem = groupBy === 'Interval' ? 'date' : 'name'; + const groupByData = + groupBy === 'Category' + ? 'groupedData' + : groupBy === 'Interval' + ? 'intervalData' + : 'data'; + const metadata = data[groupByData]; type RenderRowProps = { index: number; @@ -46,12 +52,11 @@ export function ReportTableList({ }: RenderRowProps) { const item = parent_index === undefined - ? data[index] - : data[parent_index].categories[index]; + ? metadata[index] + : metadata[parent_index].categories[index]; return renderItem({ item, - groupByItem, mode, intervalsCount, compact, @@ -62,9 +67,9 @@ export function ReportTableList({ return ( <View> - {data ? ( + {metadata ? ( <View> - {data.map((item, index) => { + {metadata.map((item, index) => { return ( <View key={item.id}> <RenderRow diff --git a/packages/desktop-client/src/components/reports/graphs/tableGraph/ReportTableRow.tsx b/packages/desktop-client/src/components/reports/graphs/tableGraph/ReportTableRow.tsx index fabe9698f..54c1c650f 100644 --- a/packages/desktop-client/src/components/reports/graphs/tableGraph/ReportTableRow.tsx +++ b/packages/desktop-client/src/components/reports/graphs/tableGraph/ReportTableRow.tsx @@ -6,33 +6,74 @@ import { integerToCurrency, } from 'loot-core/src/shared/util'; import { type DataEntity } from 'loot-core/src/types/models/reports'; +import { type RuleConditionEntity } from 'loot-core/types/models/rule'; +import { useAccounts } from '../../../../hooks/useAccounts'; +import { useCategories } from '../../../../hooks/useCategories'; +import { useNavigate } from '../../../../hooks/useNavigate'; +import { useResponsive } from '../../../../ResponsiveProvider'; import { type CSSProperties, theme } from '../../../../style'; import { Row, Cell } from '../../../table'; +import { showActivity } from '../showActivity'; type ReportTableRowProps = { item: DataEntity; balanceTypeOp: 'totalAssets' | 'totalDebts' | 'totalTotals'; - groupByItem: 'id' | 'name'; + groupBy: string; mode: string; + filters?: RuleConditionEntity[]; + startDate?: string; + endDate?: string; intervalsCount: number; compact: boolean; style?: CSSProperties; compactStyle?: CSSProperties; + showHiddenCategories?: boolean; + showOffBudget?: boolean; }; export const ReportTableRow = memo( ({ item, balanceTypeOp, - groupByItem, + groupBy, mode, + filters = [], + startDate = '', + endDate, intervalsCount, compact, style, compactStyle, + showHiddenCategories = false, + showOffBudget = false, }: ReportTableRowProps) => { const average = amountToInteger(item[balanceTypeOp]) / intervalsCount; + const groupByItem = groupBy === 'Interval' ? 'date' : 'name'; + + const navigate = useNavigate(); + const { isNarrowWidth } = useResponsive(); + const categories = useCategories(); + const accounts = useAccounts(); + + const pointer = + !isNarrowWidth && + !['Group', 'Interval'].includes(groupBy) && + !categories.grouped.map(g => g.id).includes(item.id) + ? 'pointer' + : 'inherit'; + + const hoverUnderline = + !isNarrowWidth && + !['Group', 'Interval'].includes(groupBy) && + !categories.grouped.map(g => g.id).includes(item.id) + ? { + cursor: pointer, + ':hover': { textDecoration: 'underline' }, + flexGrow: 0, + } + : {}; + return ( <Row key={item.id} @@ -45,7 +86,7 @@ export const ReportTableRow = memo( > <Cell value={item[groupByItem]} - title={item[groupByItem].length > 12 ? item[groupByItem] : undefined} + title={item[groupByItem] ?? undefined} style={{ width: compact ? 80 : 125, flexShrink: 0, @@ -60,6 +101,7 @@ export const ReportTableRow = memo( style={{ minWidth: compact ? 50 : 85, }} + linkStyle={hoverUnderline} valueStyle={compactStyle} value={amountToCurrency(intervalItem[balanceTypeOp])} title={ @@ -67,6 +109,24 @@ export const ReportTableRow = memo( ? amountToCurrency(intervalItem[balanceTypeOp]) : undefined } + onClick={() => + !isNarrowWidth && + !['Group', 'Interval'].includes(groupBy) && + !categories.grouped.map(g => g.id).includes(item.id) && + showActivity({ + navigate, + categories, + accounts, + balanceTypeOp, + filters, + showHiddenCategories, + showOffBudget, + type: 'time', + startDate: intervalItem.dateLookup || '', + field: groupBy.toLowerCase(), + id: item.id, + }) + } width="flex" privacyFilter /> @@ -86,7 +146,27 @@ export const ReportTableRow = memo( style={{ minWidth: compact ? 50 : 85, }} + linkStyle={hoverUnderline} valueStyle={compactStyle} + onClick={() => + !isNarrowWidth && + !['Group', 'Interval'].includes(groupBy) && + !categories.grouped.map(g => g.id).includes(item.id) && + showActivity({ + navigate, + categories, + accounts, + balanceTypeOp, + filters, + showHiddenCategories, + showOffBudget, + type: 'assets', + startDate, + endDate, + field: groupBy.toLowerCase(), + id: item.id, + }) + } /> <Cell value={amountToCurrency(item.totalDebts)} @@ -100,7 +180,27 @@ export const ReportTableRow = memo( style={{ minWidth: compact ? 50 : 85, }} + linkStyle={hoverUnderline} valueStyle={compactStyle} + onClick={() => + !isNarrowWidth && + !['Group', 'Interval'].includes(groupBy) && + !categories.grouped.map(g => g.id).includes(item.id) && + showActivity({ + navigate, + categories, + accounts, + balanceTypeOp, + filters, + showHiddenCategories, + showOffBudget, + type: 'debts', + startDate, + endDate, + field: groupBy.toLowerCase(), + id: item.id, + }) + } /> </> )} @@ -115,7 +215,27 @@ export const ReportTableRow = memo( fontWeight: 600, minWidth: compact ? 50 : 85, }} + linkStyle={hoverUnderline} valueStyle={compactStyle} + onClick={() => + !isNarrowWidth && + !['Group', 'Interval'].includes(groupBy) && + !categories.grouped.map(g => g.id).includes(item.id) && + showActivity({ + navigate, + categories, + accounts, + balanceTypeOp, + filters, + showHiddenCategories, + showOffBudget, + type: 'totals', + startDate, + endDate, + field: groupBy.toLowerCase(), + id: item.id, + }) + } width="flex" privacyFilter /> diff --git a/packages/desktop-client/src/components/reports/graphs/tableGraph/ReportTableTotals.tsx b/packages/desktop-client/src/components/reports/graphs/tableGraph/ReportTableTotals.tsx index 8cae27600..48b16e581 100644 --- a/packages/desktop-client/src/components/reports/graphs/tableGraph/ReportTableTotals.tsx +++ b/packages/desktop-client/src/components/reports/graphs/tableGraph/ReportTableTotals.tsx @@ -8,12 +8,18 @@ import { integerToCurrency, } from 'loot-core/src/shared/util'; import { type GroupedEntity } from 'loot-core/src/types/models/reports'; +import { type RuleConditionEntity } from 'loot-core/types/models/rule'; +import { useAccounts } from '../../../../hooks/useAccounts'; +import { useCategories } from '../../../../hooks/useCategories'; +import { useNavigate } from '../../../../hooks/useNavigate'; +import { useResponsive } from '../../../../ResponsiveProvider'; import { theme } from '../../../../style'; import { styles } from '../../../../style/styles'; import { type CSSProperties } from '../../../../style/types'; import { View } from '../../../common/View'; import { Row, Cell } from '../../../table'; +import { showActivity } from '../showActivity'; type ReportTableTotalsProps = { data: GroupedEntity; @@ -25,6 +31,10 @@ type ReportTableTotalsProps = { compact: boolean; style?: CSSProperties; compactStyle?: CSSProperties; + groupBy: string; + filters?: RuleConditionEntity[]; + showHiddenCategories?: boolean; + showOffBudget?: boolean; }; export function ReportTableTotals({ @@ -37,6 +47,10 @@ export function ReportTableTotals({ compact, style, compactStyle, + groupBy, + filters = [], + showHiddenCategories = false, + showOffBudget = false, }: ReportTableTotalsProps) { const [scrollWidthTotals, setScrollWidthTotals] = useState(0); @@ -44,15 +58,34 @@ export function ReportTableTotals({ if (totalScrollRef.current) { const [parent, child] = [ totalScrollRef.current.offsetParent - ? totalScrollRef.current.parentElement.scrollHeight + ? totalScrollRef.current.parentElement.scrollHeight || 0 : 0, totalScrollRef.current ? totalScrollRef.current.scrollHeight : 0, ]; setScrollWidthTotals(parent > 0 && child > 0 && parent - child); } }); - const average = amountToInteger(data[balanceTypeOp]) / intervalsCount; + + const navigate = useNavigate(); + const { isNarrowWidth } = useResponsive(); + const categories = useCategories(); + const accounts = useAccounts(); + + const pointer = + !isNarrowWidth && !['Group', 'Interval'].includes(groupBy) + ? 'pointer' + : 'inherit'; + + const hoverUnderline = + !isNarrowWidth && !['Group', 'Interval'].includes(groupBy) + ? { + cursor: pointer, + ':hover': { textDecoration: 'underline' }, + flexGrow: 0, + } + : {}; + return ( <Row collapsed={true} @@ -99,6 +132,7 @@ export function ReportTableTotals({ style={{ minWidth: compact ? 50 : 85, }} + linkStyle={hoverUnderline} valueStyle={compactStyle} key={amountToCurrency(item[balanceTypeOp])} value={amountToCurrency(item[balanceTypeOp])} @@ -107,6 +141,21 @@ export function ReportTableTotals({ ? amountToCurrency(item[balanceTypeOp]) : undefined } + onClick={() => + !isNarrowWidth && + !['Group', 'Interval'].includes(groupBy) && + showActivity({ + navigate, + categories, + accounts, + balanceTypeOp, + filters, + showHiddenCategories, + showOffBudget, + type: 'time', + startDate: item.dateStart || '', + }) + } width="flex" privacyFilter /> @@ -118,6 +167,7 @@ export function ReportTableTotals({ style={{ minWidth: compact ? 50 : 85, }} + linkStyle={hoverUnderline} valueStyle={compactStyle} value={amountToCurrency(data.totalAssets)} title={ @@ -125,6 +175,22 @@ export function ReportTableTotals({ ? amountToCurrency(data.totalAssets) : undefined } + onClick={() => + !isNarrowWidth && + !['Group', 'Interval'].includes(groupBy) && + showActivity({ + navigate, + categories, + accounts, + balanceTypeOp, + filters, + showHiddenCategories, + showOffBudget, + type: 'assets', + startDate: data.startDate || '', + endDate: data.endDate, + }) + } width="flex" privacyFilter /> @@ -132,6 +198,7 @@ export function ReportTableTotals({ style={{ minWidth: compact ? 50 : 85, }} + linkStyle={hoverUnderline} valueStyle={compactStyle} value={amountToCurrency(data.totalDebts)} title={ @@ -139,6 +206,22 @@ export function ReportTableTotals({ ? amountToCurrency(data.totalDebts) : undefined } + onClick={() => + !isNarrowWidth && + !['Group', 'Interval'].includes(groupBy) && + showActivity({ + navigate, + categories, + accounts, + balanceTypeOp, + filters, + showHiddenCategories, + showOffBudget, + type: 'debts', + startDate: data.startDate || '', + endDate: data.endDate, + }) + } width="flex" privacyFilter /> @@ -148,6 +231,7 @@ export function ReportTableTotals({ style={{ minWidth: compact ? 50 : 85, }} + linkStyle={hoverUnderline} valueStyle={compactStyle} value={amountToCurrency(data[balanceTypeOp])} title={ @@ -155,6 +239,22 @@ export function ReportTableTotals({ ? amountToCurrency(data[balanceTypeOp]) : undefined } + onClick={() => + !isNarrowWidth && + !['Group', 'Interval'].includes(groupBy) && + showActivity({ + navigate, + categories, + accounts, + balanceTypeOp, + filters, + showHiddenCategories, + showOffBudget, + type: 'totals', + startDate: data.startDate || '', + endDate: data.endDate, + }) + } width="flex" privacyFilter /> diff --git a/packages/desktop-client/src/components/table.tsx b/packages/desktop-client/src/components/table.tsx index 96fd84131..cfd2ad90e 100644 --- a/packages/desktop-client/src/components/table.tsx +++ b/packages/desktop-client/src/components/table.tsx @@ -113,7 +113,8 @@ export const Field = forwardRef<HTMLDivElement, FieldProps>(function Field( export function UnexposedCellContent({ value, formatter, -}: Pick<CellProps, 'value' | 'formatter'>) { + linkStyle, +}: Pick<CellProps, 'value' | 'formatter' | 'linkStyle'>) { return ( <Text style={{ @@ -121,6 +122,7 @@ export function UnexposedCellContent({ whiteSpace: 'nowrap', overflow: 'hidden', textOverflow: 'ellipsis', + ...linkStyle, }} > {formatter ? formatter(value) : value} @@ -139,6 +141,7 @@ type CellProps = Omit<ComponentProps<typeof View>, 'children' | 'value'> & { unexposedContent?: ReactNode; value?: string; valueStyle?: CSSProperties; + linkStyle?: CSSProperties; onExpose?: (name: string) => void; privacyFilter?: ComponentProps< typeof ConditionalPrivacyFilter @@ -158,6 +161,7 @@ export function Cell({ plain, style, valueStyle, + linkStyle, unexposedContent, privacyFilter, ...viewProps @@ -229,7 +233,11 @@ export function Cell({ } > {unexposedContent || ( - <UnexposedCellContent value={value} formatter={formatter} /> + <UnexposedCellContent + linkStyle={linkStyle} + value={value} + formatter={formatter} + /> )} </View> )} diff --git a/packages/loot-core/src/types/models/reports.d.ts b/packages/loot-core/src/types/models/reports.d.ts index eb9fe981a..f1c01d6ab 100644 --- a/packages/loot-core/src/types/models/reports.d.ts +++ b/packages/loot-core/src/types/models/reports.d.ts @@ -86,6 +86,7 @@ export type ItemEntity = { export type IntervalData = { date: string; + dateLookup: string; totalAssets: number; totalDebts: number; totalTotals: number; @@ -95,6 +96,7 @@ export interface DataEntity { id: string; name: string; date?: string; + dateStart?: string; intervalData: IntervalData[]; categories?: ItemEntity[]; totalAssets: number; diff --git a/upcoming-release-notes/2696.md b/upcoming-release-notes/2696.md new file mode 100644 index 000000000..588b982ac --- /dev/null +++ b/upcoming-release-notes/2696.md @@ -0,0 +1,6 @@ +--- +category: Enhancements +authors: [carkom] +--- + +Custom Reports - show transactions when table cell is clicked. -- GitLab