Skip to content

Commit

Permalink
✨ (dashboards) ability to save cash-flow report filters/date-range (#…
Browse files Browse the repository at this point in the history
  • Loading branch information
MatissJanis authored Sep 13, 2024
1 parent 933804e commit 183c4b2
Show file tree
Hide file tree
Showing 19 changed files with 222 additions and 67 deletions.
1 change: 1 addition & 0 deletions .eslintignore
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ packages/desktop-client/**/node_modules/*
packages/desktop-client/node_modules/
packages/desktop-client/src/icons/**/*
packages/desktop-client/test-results/
packages/desktop-client/playwright-report/

packages/desktop-electron/client-build/
packages/desktop-electron/dist/
Expand Down
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
18 changes: 10 additions & 8 deletions packages/desktop-client/src/components/reports/Header.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,6 @@ import {
} from 'loot-core/types/models';

import { useFeatureFlag } from '../../hooks/useFeatureFlag';
import { SvgPause, SvgPlay } from '../../icons/v1';
import { useResponsive } from '../../ResponsiveProvider';
import { Button } from '../common/Button2';
import { Select } from '../common/Select';
Expand All @@ -17,6 +16,7 @@ import { AppliedFilters } from '../filters/AppliedFilters';
import { FilterButton } from '../filters/FiltersMenu';

import {
calculateTimeRange,
getFullRange,
getLatestRange,
validateEnd,
Expand Down Expand Up @@ -85,16 +85,18 @@ export function Header({
{isDashboardsFeatureEnabled && mode && (
<Button
variant={mode === 'static' ? 'normal' : 'primary'}
onPress={() =>
onChangeDates(
onPress={() => {
const newMode = mode === 'static' ? 'sliding-window' : 'static';
const [newStart, newEnd] = calculateTimeRange({
start,
end,
mode === 'static' ? 'sliding-window' : 'static',
)
}
Icon={mode === 'static' ? SvgPause : SvgPlay}
mode: newMode,
});

onChangeDates(newStart, newEnd, newMode);
}}
>
{mode === 'static' ? 'Paused' : 'Live'}
{mode === 'static' ? 'Static' : 'Live'}
</Button>
)}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -537,6 +537,7 @@ export function Overview() {
/>
) : item.type === 'cash-flow-card' ? (
<CashFlowCard
widgetId={item.i}
isEditing={isEditing}
meta={item.meta}
onMetaChange={newMeta => onMetaChange(item, newMeta)}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ export function ReportRouter() {
<Route path="/net-worth" element={<NetWorth />} />
<Route path="/net-worth/:id" element={<NetWorth />} />
<Route path="/cash-flow" element={<CashFlow />} />
<Route path="/cash-flow/:id" element={<CashFlow />} />
<Route path="/custom" element={<CustomReport />} />
<Route path="/spending" element={<Spending />} />
</Routes>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,7 @@ type CustomTooltipProps = TooltipProps<number, 'date'> & {
function CustomTooltip({ active, payload, isConcise }: CustomTooltipProps) {
const { t } = useTranslation();

if (!active || !payload) {
if (!active || !payload || !Array.isArray(payload) || !payload[0]) {
return null;
}

Expand Down
17 changes: 11 additions & 6 deletions packages/desktop-client/src/components/reports/reportRanges.ts
Original file line number Diff line number Diff line change
Expand Up @@ -166,18 +166,23 @@ export function getLatestRange(offset: number) {
}

export function calculateTimeRange(
{ start, end, mode }: TimeFrame = {
start: monthUtils.subMonths(monthUtils.currentMonth(), 5),
end: monthUtils.currentMonth(),
mode: 'sliding-window',
},
timeFrame?: TimeFrame,
defaultTimeFrame?: TimeFrame,
) {
const start =
timeFrame?.start ??
defaultTimeFrame?.start ??
monthUtils.subMonths(monthUtils.currentMonth(), 5);
const end =
timeFrame?.end ?? defaultTimeFrame?.end ?? monthUtils.currentMonth();
const mode = timeFrame?.mode ?? defaultTimeFrame?.mode ?? 'sliding-window';

if (mode === 'full') {
return getFullRange(start);
}
if (mode === 'sliding-window') {
return getLatestRange(monthUtils.differenceInCalendarMonths(end, start));
}

return [start, end, 'static'];
return [start, end, 'static'] as const;
}
145 changes: 114 additions & 31 deletions packages/desktop-client/src/components/reports/reports/CashFlow.tsx
Original file line number Diff line number Diff line change
@@ -1,11 +1,20 @@
import React, { useState, useEffect, useMemo } from 'react';
import { Trans, useTranslation } from 'react-i18next';
import { useDispatch } from 'react-redux';
import { useParams } from 'react-router-dom';

import * as d from 'date-fns';

import { addNotification } from 'loot-core/client/actions';
import { useWidget } from 'loot-core/client/data-hooks/widget';
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 { type RuleConditionEntity } from 'loot-core/types/models';
import {
type CashFlowWidget,
type RuleConditionEntity,
type TimeFrame,
} from 'loot-core/types/models';

import { useFilters } from '../../../hooks/useFilters';
import { useNavigate } from '../../../hooks/useNavigate';
Expand All @@ -23,27 +32,63 @@ import { PrivacyFilter } from '../../PrivacyFilter';
import { Change } from '../Change';
import { CashFlowGraph } from '../graphs/CashFlowGraph';
import { Header } from '../Header';
import { LoadingIndicator } from '../LoadingIndicator';
import { calculateTimeRange } from '../reportRanges';
import { cashFlowByDate } from '../spreadsheets/cash-flow-spreadsheet';
import { useReport } from '../useReport';

export const defaultTimeFrame = {
start: monthUtils.dayFromDate(monthUtils.currentMonth()),
end: monthUtils.currentDay(),
mode: 'sliding-window',
} satisfies TimeFrame;

export function CashFlow() {
const params = useParams();
const { data: widget, isLoading } = useWidget<CashFlowWidget>(
params.id ?? '',
'cash-flow-card',
);

if (isLoading) {
return <LoadingIndicator />;
}

return <CashFlowInner widget={widget} />;
}

type CashFlowInnerProps = {
widget: CashFlowWidget;
};

function CashFlowInner({ widget }: CashFlowInnerProps) {
const dispatch = useDispatch();
const { t } = useTranslation();

const {
conditions,
conditionsOp,
onApply: onApplyFilter,
onDelete: onDeleteFilter,
onUpdate: onUpdateFilter,
onConditionsOpChange,
} = useFilters<RuleConditionEntity>();
} = useFilters<RuleConditionEntity>(
widget?.meta?.conditions,
widget?.meta?.conditionsOp,
);

const [allMonths, setAllMonths] = useState<null | Array<{
name: string;
pretty: string;
}>>(null);
const [start, setStart] = useState(
monthUtils.subMonths(monthUtils.currentMonth(), 5),

const [initialStart, initialEnd, initialMode] = calculateTimeRange(
widget?.meta?.timeFrame,
defaultTimeFrame,
);
const [end, setEnd] = useState(monthUtils.currentDay());
const [start, setStart] = useState(initialStart);
const [end, setEnd] = useState(initialEnd);
const [mode, setMode] = useState(initialMode);
const [showBalance, setShowBalance] = useState(true);

const [isConcise, setIsConcise] = useState(() => {
Expand Down Expand Up @@ -80,52 +125,72 @@ export function CashFlow() {
run();
}, []);

function onChangeDates(start: string, end: string) {
function onChangeDates(start: string, end: string, mode: TimeFrame['mode']) {
const numDays = d.differenceInCalendarDays(
d.parseISO(end),
d.parseISO(start),
);
const isConcise = numDays > 31 * 3;

let endDay = end + '-31';
if (endDay > monthUtils.currentDay()) {
endDay = monthUtils.currentDay();
}

setStart(start + '-01');
setEnd(endDay);
setStart(start);
setEnd(end);
setMode(mode);
setIsConcise(isConcise);
}

const navigate = useNavigate();
const { isNarrowWidth } = useResponsive();

async function onSaveWidget() {
await send('dashboard-update-widget', {
id: widget?.id,
meta: {
...(widget.meta ?? {}),
conditions,
conditionsOp,
timeFrame: {
start,
end,
mode,
},
},
});
dispatch(
addNotification({
type: 'message',
message: t('Dashboard widget successfully saved.'),
}),
);
}

if (!allMonths || !data) {
return null;
}

const { graphData, totalExpenses, totalIncome, totalTransfers } = data;
const title = widget?.meta?.name ?? t('Cash Flow');

return (
<Page
header={
isNarrowWidth ? (
<MobilePageHeader
title="Cash Flow"
title={title}
leftContent={
<MobileBackButton onPress={() => navigate('/reports')} />
}
/>
) : (
<PageHeader title="Cash Flow" />
<PageHeader title={title} />
)
}
padding={0}
>
<Header
allMonths={allMonths}
start={monthUtils.getMonth(start)}
end={monthUtils.getMonth(end)}
start={start}
end={end}
mode={mode}
show1Month
onChangeDates={onChangeDates}
onApply={onApplyFilter}
Expand All @@ -135,9 +200,17 @@ export function CashFlow() {
conditionsOp={conditionsOp}
onConditionsOpChange={onConditionsOpChange}
>
<Button onPress={() => setShowBalance(state => !state)}>
{showBalance ? 'Hide balance' : 'Show balance'}
</Button>
<View style={{ flexDirection: 'row', gap: 10 }}>
<Button onPress={() => setShowBalance(state => !state)}>
{showBalance ? t('Hide balance') : t('Show balance')}
</Button>

{widget && (
<Button variant="primary" onPress={onSaveWidget}>
<Trans>Save widget</Trans>
</Button>
)}
</View>
</Header>
<View
style={{
Expand Down Expand Up @@ -167,7 +240,11 @@ export function CashFlow() {

<AlignedText
style={{ marginBottom: 5, minWidth: 160 }}
left={<Block>Expenses:</Block>}
left={
<Block>
<Trans>Expenses:</Trans>
</Block>
}
right={
<Text style={{ fontWeight: 600 }}>
<PrivacyFilter>
Expand All @@ -179,7 +256,11 @@ export function CashFlow() {

<AlignedText
style={{ marginBottom: 5, minWidth: 160 }}
left={<Block>Transfers:</Block>}
left={
<Block>
<Trans>Transfers:</Trans>
</Block>
}
right={
<Text style={{ fontWeight: 600 }}>
<PrivacyFilter>
Expand Down Expand Up @@ -207,15 +288,17 @@ export function CashFlow() {
userSelect: 'none',
}}
>
<Paragraph>
<strong>How is cash flow calculated?</strong>
</Paragraph>
<Paragraph>
Cash flow shows the balance of your budgeted accounts over time, and
the amount of expenses/income each day or month. Your budgeted
accounts are considered to be “cash on hand,” so this gives you a
picture of how available money fluctuates.
</Paragraph>
<Trans>
<Paragraph>
<strong>How is cash flow calculated?</strong>
</Paragraph>
<Paragraph>
Cash flow shows the balance of your budgeted accounts over time,
and the amount of expenses/income each day or month. Your budgeted
accounts are considered to be “cash on hand,” so this gives you a
picture of how available money fluctuates.
</Paragraph>
</Trans>
</View>
</View>
</Page>
Expand Down
Loading

0 comments on commit 183c4b2

Please sign in to comment.