Custom Reports convert all jsx files (#2707)

* Overview

* Router and Selector

* ReportSidebar

* ReportTopbar

* CustomReport

* updates

* notes

* Patch some TS issues

* fix malfunctioning disabled lists

* add find

---------

Co-authored-by: Matiss Janis Aboltins <matiss@mja.lv>
This commit is contained in:
Neil
2024-05-07 17:59:51 +01:00
committed by GitHub
parent f20e9c6bdb
commit dcb25bb5e6
16 changed files with 471 additions and 230 deletions

View File

@@ -36,7 +36,7 @@ type CategoryAutocompleteItem = CategoryEntity & {
group?: CategoryGroupEntity;
};
export type CategoryListProps = {
type CategoryListProps = {
items: CategoryAutocompleteItem[];
getItemProps?: (arg: {
item: CategoryAutocompleteItem;

View File

@@ -12,7 +12,6 @@ import {
SvgViewHide,
SvgViewShow,
} from '../../icons/v2';
import { type CategoryListProps } from '../autocomplete/CategoryAutocomplete';
import { Button } from '../common/Button';
import { Text } from '../common/Text';
import { View } from '../common/View';
@@ -22,8 +21,8 @@ import { GraphButton } from './GraphButton';
type CategorySelectorProps = {
categoryGroups: Array<CategoryGroupEntity>;
selectedCategories: CategoryListProps['items'];
setSelectedCategories: (selectedCategories: CategoryEntity[]) => null;
selectedCategories: CategoryEntity[];
setSelectedCategories: (selectedCategories: CategoryEntity[]) => void;
showHiddenCategories?: boolean;
};

View File

@@ -32,11 +32,9 @@ export function Overview() {
<View
style={{
...styles.page,
...{
padding: 15,
paddingTop: 0,
minWidth: isNarrowWidth ? null : 700,
},
padding: 15,
paddingTop: 0,
minWidth: isNarrowWidth ? undefined : 700,
}}
>
{customReportsFeatureFlag && !isNarrowWidth && (

View File

@@ -44,95 +44,118 @@ const groupByOptions = [
{ description: 'Interval' },
];
const dateRangeOptions = [
export type dateRangeProps = {
description: string;
name: number | string;
type?: string;
Daily: boolean;
Weekly: boolean;
Monthly: boolean;
Yearly: boolean;
};
const dateRangeOptions: dateRangeProps[] = [
{
description: 'This week',
type: 'Weeks',
name: 0,
Yearly: false,
Monthly: false,
type: 'Weeks',
Daily: true,
Weekly: true,
Monthly: false,
Yearly: false,
},
{
description: 'Last week',
type: 'Weeks',
name: 1,
Yearly: false,
Monthly: false,
type: 'Weeks',
Daily: true,
Weekly: true,
Monthly: false,
Yearly: false,
},
{
description: 'This month',
type: 'Months',
name: 0,
Yearly: false,
Monthly: true,
type: 'Months',
Daily: true,
Weekly: true,
Monthly: true,
Yearly: false,
},
{
description: 'Last month',
type: 'Months',
name: 1,
Yearly: false,
Monthly: true,
type: 'Months',
Daily: true,
Weekly: true,
Monthly: true,
Yearly: false,
},
{
description: 'Last 3 months',
type: 'Months',
name: 2,
Yearly: false,
Monthly: true,
type: 'Months',
Daily: true,
Weekly: true,
Monthly: true,
Yearly: false,
},
{
description: 'Last 6 months',
type: 'Months',
name: 5,
Yearly: false,
Monthly: true,
type: 'Months',
Daily: false,
Weekly: false,
Monthly: true,
Yearly: false,
},
{
description: 'Last 12 months',
type: 'Months',
name: 11,
Yearly: false,
Monthly: true,
type: 'Months',
Daily: false,
Weekly: false,
Monthly: true,
Yearly: false,
},
{
description: 'Year to date',
name: 'yearToDate',
Yearly: true,
Monthly: true,
Daily: true,
Weekly: true,
Monthly: true,
Yearly: true,
},
{
description: 'Last year',
name: 'lastYear',
Yearly: true,
Monthly: true,
Daily: true,
Weekly: true,
Monthly: true,
Yearly: true,
},
{
description: 'All time',
name: 'allTime',
Yearly: true,
Monthly: true,
Daily: true,
Weekly: true,
Monthly: true,
Yearly: true,
},
];
const intervalOptions = [
type intervalOptionsProps = {
description: string;
name: 'Day' | 'Week' | 'Month' | 'Year';
format: string;
range:
| 'dayRangeInclusive'
| 'weekRangeInclusive'
| 'rangeInclusive'
| 'yearRangeInclusive';
};
const intervalOptions: intervalOptionsProps[] = [
{
description: 'Daily',
name: 'Day',
@@ -175,15 +198,19 @@ export const ReportOptions = {
dateRangeOptions.map(item => [item.description, item.type]),
),
interval: intervalOptions,
intervalMap: new Map(
intervalMap: new Map<string, 'Day' | 'Week' | 'Month' | 'Year'>(
intervalOptions.map(item => [item.description, item.name]),
),
intervalFormat: new Map(
intervalOptions.map(item => [item.description, item.format]),
),
intervalRange: new Map(
intervalOptions.map(item => [item.description, item.range]),
),
intervalRange: new Map<
string,
| 'dayRangeInclusive'
| 'weekRangeInclusive'
| 'rangeInclusive'
| 'yearRangeInclusive'
>(intervalOptions.map(item => [item.description, item.range])),
};
export type QueryDataEntity = {

View File

@@ -1,8 +1,12 @@
import React, { useState } from 'react';
import * as monthUtils from 'loot-core/src/shared/months';
import { type CategoryEntity } from 'loot-core/types/models/category';
import { type CategoryGroupEntity } from 'loot-core/types/models/category-group';
import { type CustomReportEntity } from 'loot-core/types/models/reports';
import { type LocalPrefs } from 'loot-core/types/prefs';
import { theme } from '../../style';
import { theme } from '../../style/theme';
import { Button } from '../common/Button';
import { Menu } from '../common/Menu';
import { Select } from '../common/Select';
@@ -14,10 +18,42 @@ import { CategorySelector } from './CategorySelector';
import { defaultsList } from './disabledList';
import { getLiveRange } from './getLiveRange';
import { ModeButton } from './ModeButton';
import { ReportOptions } from './ReportOptions';
import { type dateRangeProps, ReportOptions } from './ReportOptions';
import { validateEnd, validateStart } from './reportRanges';
import { setSessionReport } from './setSessionReport';
type ReportSidebarProps = {
customReportItems: CustomReportEntity;
categories: { list: CategoryEntity[]; grouped: CategoryGroupEntity[] };
dateRangeLine: number;
allIntervals: { name: string; pretty: string }[];
setDateRange: (value: string) => void;
setGraphType: (value: string) => void;
setGroupBy: (value: string) => void;
setInterval: (value: string) => void;
setBalanceType: (value: string) => void;
setMode: (value: string) => void;
setIsDateStatic: (value: boolean) => void;
setShowEmpty: (value: boolean) => void;
setShowOffBudget: (value: boolean) => void;
setShowHiddenCategories: (value: boolean) => void;
setShowUncategorized: (value: boolean) => void;
setSelectedCategories: (value: CategoryEntity[]) => void;
onChangeDates: (dateStart: string, dateEnd: string) => void;
onReportChange: ({
savedReport,
type,
}: {
savedReport?: CustomReportEntity;
type: string;
}) => void;
disabledItems: (type: string) => string[];
defaultItems: (item: string) => void;
defaultModeItems: (graph: string, item: string) => void;
earliestTransaction: string;
firstDayOfWeekIdx: LocalPrefs['firstDayOfWeekIdx'];
};
export function ReportSidebar({
customReportItems,
categories,
@@ -42,9 +78,9 @@ export function ReportSidebar({
defaultModeItems,
earliestTransaction,
firstDayOfWeekIdx,
}) {
}: ReportSidebarProps) {
const [menuOpen, setMenuOpen] = useState(false);
const onSelectRange = cond => {
const onSelectRange = (cond: string) => {
setSessionReport('dateRange', cond);
onReportChange({ type: 'modify' });
setDateRange(cond);
@@ -53,11 +89,11 @@ export function ReportSidebar({
);
};
const onChangeMode = cond => {
const onChangeMode = (cond: string) => {
setSessionReport('mode', cond);
onReportChange({ type: 'modify' });
setMode(cond);
let graph;
let graph = '';
if (cond === 'time') {
if (customReportItems.graphType === 'BarGraph') {
setSessionReport('graphType', 'StackedBarGraph');
@@ -74,14 +110,14 @@ export function ReportSidebar({
defaultModeItems(graph, cond);
};
const onChangeSplit = cond => {
const onChangeSplit = (cond: string) => {
setSessionReport('groupBy', cond);
onReportChange({ type: 'modify' });
setGroupBy(cond);
defaultItems(cond);
};
const onChangeBalanceType = cond => {
const onChangeBalanceType = (cond: string) => {
setSessionReport('balanceType', cond);
onReportChange({ type: 'modify' });
setBalanceType(cond);
@@ -187,11 +223,11 @@ export function ReportSidebar({
onReportChange({ type: 'modify' });
if (
ReportOptions.dateRange
.filter(int => !int[e])
.filter(d => !d[e as keyof dateRangeProps])
.map(int => int.description)
.includes(customReportItems.dateRange)
) {
onSelectRange(defaultsList.intervalRange.get(e));
onSelectRange(defaultsList.intervalRange.get(e) || '');
}
}}
options={ReportOptions.interval.map(option => [
@@ -353,9 +389,11 @@ export function ReportSidebar({
onSelectRange(e);
}}
options={ReportOptions.dateRange
.filter(f => f[customReportItems.interval])
.filter(
f => f[customReportItems.interval as keyof dateRangeProps],
)
.map(option => [option.description, option.description])}
line={dateRangeLine > 0 && dateRangeLine}
line={dateRangeLine > 0 ? dateRangeLine : undefined}
/>
</View>
) : (
@@ -385,7 +423,9 @@ export function ReportSidebar({
value={customReportItems.startDate}
defaultLabel={monthUtils.format(
customReportItems.startDate,
ReportOptions.intervalFormat.get(customReportItems.interval),
ReportOptions.intervalFormat.get(
customReportItems.interval,
) || '',
)}
options={allIntervals.map(({ name, pretty }) => [name, pretty])}
/>
@@ -415,7 +455,9 @@ export function ReportSidebar({
value={customReportItems.endDate}
defaultLabel={monthUtils.format(
customReportItems.endDate,
ReportOptions.intervalFormat.get(customReportItems.interval),
ReportOptions.intervalFormat.get(
customReportItems.interval,
) || '',
)}
options={allIntervals.map(({ name, pretty }) => [name, pretty])}
/>
@@ -443,7 +485,7 @@ export function ReportSidebar({
? true
: false;
})}
selectedCategories={customReportItems.selectedCategories}
selectedCategories={customReportItems.selectedCategories || []}
setSelectedCategories={e => {
setSelectedCategories(e);
onReportChange({ type: 'modify' });

View File

@@ -1,5 +1,8 @@
import React from 'react';
import { type CustomReportEntity } from 'loot-core/types/models/reports';
import { type RuleConditionEntity } from 'loot-core/types/models/rule';
import {
SvgCalculator,
SvgChart,
@@ -18,6 +21,27 @@ import { GraphButton } from './GraphButton';
import { SaveReport } from './SaveReport';
import { setSessionReport } from './setSessionReport';
type ReportTopbarProps = {
customReportItems: CustomReportEntity;
report: CustomReportEntity;
savedStatus: string;
setGraphType: (value: string) => void;
viewLegend: boolean;
viewSummary: boolean;
viewLabels: boolean;
onApplyFilter: (newFilter: RuleConditionEntity) => void;
onChangeViews: (viewType: string) => void;
onReportChange: ({
savedReport,
type,
}: {
savedReport?: CustomReportEntity;
type: string;
}) => void;
isItemDisabled: (type: string) => boolean;
defaultItems: (item: string) => void;
};
export function ReportTopbar({
customReportItems,
report,
@@ -29,10 +53,10 @@ export function ReportTopbar({
onApplyFilter,
onChangeViews,
onReportChange,
disabledItems,
isItemDisabled,
defaultItems,
}) {
const onChangeGraph = cond => {
}: ReportTopbarProps) {
const onChangeGraph = (cond: string) => {
setSessionReport('graphType', cond);
onReportChange({ type: 'modify' });
setGraphType(cond);
@@ -55,7 +79,7 @@ export function ReportTopbar({
onChangeGraph('TableGraph');
}}
style={{ marginRight: 15 }}
disabled={disabledItems('TableGraph')}
disabled={isItemDisabled('TableGraph')}
>
<SvgQueue width={15} height={15} />
</GraphButton>
@@ -73,7 +97,7 @@ export function ReportTopbar({
);
}}
style={{ marginRight: 15 }}
disabled={disabledItems(
disabled={isItemDisabled(
customReportItems.mode === 'total' ? 'BarGraph' : 'StackedBarGraph',
)}
>
@@ -86,7 +110,7 @@ export function ReportTopbar({
onChangeGraph('LineGraph');
}}
style={{ marginRight: 15 }}
disabled={disabledItems('LineGraph')}
disabled={isItemDisabled('LineGraph')}
>
<SvgChart width={15} height={15} />
</GraphButton>
@@ -97,7 +121,7 @@ export function ReportTopbar({
onChangeGraph('AreaGraph');
}}
style={{ marginRight: 15 }}
disabled={disabledItems('AreaGraph')}
disabled={isItemDisabled('AreaGraph')}
>
<SvgChartArea width={15} height={15} />
</GraphButton>
@@ -108,7 +132,7 @@ export function ReportTopbar({
onChangeGraph('DonutGraph');
}}
style={{ marginRight: 15 }}
disabled={disabledItems('DonutGraph')}
disabled={isItemDisabled('DonutGraph')}
>
<SvgChartPie width={15} height={15} />
</GraphButton>
@@ -128,7 +152,7 @@ export function ReportTopbar({
}}
style={{ marginRight: 15 }}
title="Show Legend"
disabled={disabledItems('ShowLegend')}
disabled={isItemDisabled('ShowLegend')}
>
<SvgListBullet width={15} height={15} />
</GraphButton>
@@ -149,7 +173,7 @@ export function ReportTopbar({
}}
style={{ marginRight: 15 }}
title="Show Labels"
disabled={disabledItems('ShowLabels')}
disabled={isItemDisabled('ShowLabels')}
>
<SvgTag width={15} height={15} />
</GraphButton>
@@ -165,11 +189,15 @@ export function ReportTopbar({
<FilterButton
compact
hover
onApply={e => {
setSessionReport('conditions', [...customReportItems.conditions, e]);
onApply={(e: RuleConditionEntity) => {
setSessionReport('conditions', [
...(customReportItems.conditions ?? []),
e,
]);
onApplyFilter(e);
onReportChange({ type: 'modify' });
}}
exclude={[]}
/>
<View style={{ flex: 1 }} />
<SaveReport

View File

@@ -17,7 +17,16 @@ const intervalOptions = [
},
];
const totalGraphOptions = [
type graphOptions = {
description: string;
disabledSplit: string[];
defaultSplit: string;
disabledType: string[];
defaultType: string;
disableLegend?: boolean;
disableLabel?: boolean;
};
const totalGraphOptions: graphOptions[] = [
{
description: 'TableGraph',
disabledSplit: [],
@@ -51,7 +60,7 @@ const totalGraphOptions = [
},
];
const timeGraphOptions = [
const timeGraphOptions: graphOptions[] = [
{
description: 'TableGraph',
disabledSplit: ['Interval'],
@@ -94,35 +103,67 @@ const modeOptions = [
},
];
export function disabledGraphList(
item: string,
newGraph: string,
type: 'disabledSplit' | 'disabledType',
) {
const graphList = modeOptions.find(d => d.description === item);
if (!graphList) {
return [];
}
const disabledList = graphList.graphs.find(e => e.description === newGraph);
if (!disabledList) {
return [];
}
return disabledList[type];
}
export function disabledLegendLabel(
item: string,
newGraph: string,
type: 'disableLegend' | 'disableLabel',
) {
const graphList = modeOptions.find(d => d.description === item);
if (!graphList) {
return false;
}
const disableLegendLabel = graphList.graphs.find(
e => e.description === newGraph,
);
if (!disableLegendLabel) {
return false;
}
return disableLegendLabel[type];
}
export function defaultsGraphList(
item: string,
newGraph: string,
type: 'defaultSplit' | 'defaultType',
) {
const graphList = modeOptions.find(d => d.description === item);
if (!graphList) {
return '';
}
const defaultItem = graphList.graphs.find(e => e.description === newGraph);
if (!defaultItem) {
return '';
}
return defaultItem[type];
}
export const disabledList = {
mode: modeOptions,
modeGraphsMap: new Map(
modeOptions.map(item => [item.description, item.disabledGraph]),
),
graphSplitMap: new Map(
modeOptions.map(item => [
item.description,
new Map([...item.graphs].map(f => [f.description, f.disabledSplit])),
]),
),
graphTypeMap: new Map(
modeOptions.map(item => [
item.description,
new Map([...item.graphs].map(f => [f.description, f.disabledType])),
]),
),
graphLegendMap: new Map(
modeOptions.map(item => [
item.description,
new Map([...item.graphs].map(f => [f.description, f.disableLegend])),
]),
),
graphLabelsMap: new Map(
modeOptions.map(item => [
item.description,
new Map([...item.graphs].map(f => [f.description, f.disableLabel])),
]),
),
};
export const defaultsList = {
@@ -130,18 +171,6 @@ export const defaultsList = {
modeGraphsMap: new Map(
modeOptions.map(item => [item.description, item.defaultGraph]),
),
graphSplitMap: new Map(
modeOptions.map(item => [
item.description,
new Map([...item.graphs].map(f => [f.description, f.defaultSplit])),
]),
),
graphTypeMap: new Map(
modeOptions.map(item => [
item.description,
new Map([...item.graphs].map(f => [f.description, f.defaultType])),
]),
),
intervalRange: new Map(
intervalOptions.map(item => [item.description, item.defaultRange]),
),

View File

@@ -8,9 +8,9 @@ export function getLiveRange(
cond: string,
earliestTransaction: string,
firstDayOfWeekIdx?: LocalPrefs['firstDayOfWeekIdx'],
) {
let dateStart;
let dateEnd;
): [string, string] {
let dateStart = earliestTransaction;
let dateEnd = monthUtils.currentDay();
const rangeName = ReportOptions.dateRangeMap.get(cond);
switch (rangeName) {
case 'yearToDate':

View File

@@ -7,7 +7,7 @@ export function validateStart(
end: string,
interval?: string,
firstDayOfWeekIdx?: LocalPrefs['firstDayOfWeekIdx'],
) {
): [string, string] {
let addDays;
let dateStart;
switch (interval) {
@@ -47,7 +47,7 @@ export function validateEnd(
end: string,
interval?: string,
firstDayOfWeekIdx?: LocalPrefs['firstDayOfWeekIdx'],
) {
): [string, string] {
let subDays;
let dateEnd;
switch (interval) {
@@ -98,7 +98,7 @@ function boundedRange(
end: string,
interval?: string,
firstDayOfWeekIdx?: LocalPrefs['firstDayOfWeekIdx'],
) {
): [string, string] {
let latest;
switch (interval) {
case 'Daily':

View File

@@ -6,27 +6,44 @@ import * as d from 'date-fns';
import { send } from 'loot-core/src/platform/client/fetch';
import * as monthUtils from 'loot-core/src/shared/months';
import { amountToCurrency } from 'loot-core/src/shared/util';
import { type CategoryEntity } from 'loot-core/types/models/category';
import {
type GroupedEntity,
type CustomReportEntity,
} from 'loot-core/types/models/reports';
import { type RuleConditionEntity } from 'loot-core/types/models/rule';
import { useAccounts } from '../../../hooks/useAccounts';
import { useCategories } from '../../../hooks/useCategories';
import { useFilters } from '../../../hooks/useFilters';
import { useLocalPref } from '../../../hooks/useLocalPref';
import { usePayees } from '../../../hooks/usePayees';
import { SvgArrowLeft } from '../../../icons/v1/ArrowLeft';
import { useResponsive } from '../../../ResponsiveProvider';
import { theme, styles } from '../../../style';
import { AlignedText } from '../../common/AlignedText';
import { Block } from '../../common/Block';
import { Link } from '../../common/Link';
import { Text } from '../../common/Text';
import { View } from '../../common/View';
import { AppliedFilters } from '../../filters/AppliedFilters';
import { PrivacyFilter } from '../../PrivacyFilter';
import { ChooseGraph } from '../ChooseGraph';
import { defaultsList, disabledList } from '../disabledList';
import {
defaultsGraphList,
defaultsList,
disabledGraphList,
disabledLegendLabel,
disabledList,
} from '../disabledList';
import { getLiveRange } from '../getLiveRange';
import { Header } from '../Header';
import { LoadingIndicator } from '../LoadingIndicator';
import { ReportLegend } from '../ReportLegend';
import { ReportOptions, defaultReport } from '../ReportOptions';
import {
ReportOptions,
defaultReport,
type dateRangeProps,
} from '../ReportOptions';
import { ReportSidebar } from '../ReportSidebar';
import { ReportSummary } from '../ReportSummary';
import { ReportTopbar } from '../ReportTopbar';
@@ -60,20 +77,28 @@ export function CustomReport() {
const location = useLocation();
const prevUrl = sessionStorage.getItem('url');
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 reportFromSessionStorage = sessionStorage.getItem('report');
const session = reportFromSessionStorage
? JSON.parse(reportFromSessionStorage)
: {};
const combine = location.state
? location.state.report ?? defaultReport
: defaultReport;
const loadReport = { ...combine, ...session };
const [allIntervals, setAllIntervals] = useState(null);
const [allIntervals, setAllIntervals] = useState<
Array<{
name: string;
pretty: string;
}>
>([]);
const [selectedCategories, setSelectedCategories] = useState(
loadReport.selectedCategories,
@@ -98,7 +123,8 @@ export function CustomReport() {
const [dateRange, setDateRange] = useState(loadReport.dateRange);
const [dataCheck, setDataCheck] = useState(false);
const dateRangeLine =
ReportOptions.dateRange.filter(f => f[interval]).length - 3;
ReportOptions.dateRange.filter(f => f[interval as keyof dateRangeProps])
.length - 3;
const [intervals, setIntervals] = useState(
monthUtils.rangeInclusive(startDate, endDate),
@@ -122,34 +148,62 @@ export function CustomReport() {
useEffect(() => {
async function run() {
onApplyFilter(null);
report.conditions.forEach(condition => onApplyFilter(condition));
report.conditions.forEach((condition: RuleConditionEntity) =>
onApplyFilter(condition),
);
const trans = await send('get-earliest-transaction');
setEarliestTransaction(trans ? trans.date : monthUtils.currentDay());
const format =
ReportOptions.intervalMap.get(interval).toLowerCase() + 'FromDate';
const currentInterval =
monthUtils['current' + ReportOptions.intervalMap.get(interval)]();
const earliestInterval = trans
? monthUtils[format](d.parseISO(fromDateRepr(trans.date)))
: currentInterval;
const rangeProps =
const fromDate =
interval === 'Weekly'
? [earliestInterval, currentInterval, firstDayOfWeekIdx]
: [earliestInterval, currentInterval];
const allInter = monthUtils[ReportOptions.intervalRange.get(interval)](
...rangeProps,
)
.map(inter => ({
? 'dayFromDate'
: (((ReportOptions.intervalMap.get(interval) || 'Day').toLowerCase() +
'FromDate') as 'dayFromDate' | 'monthFromDate' | 'yearFromDate');
const currentDate =
interval === 'Weekly'
? 'currentDay'
: (('current' +
(ReportOptions.intervalMap.get(interval) || 'Day')) as
| 'currentDay'
| 'currentMonth'
| 'currentYear');
const currentInterval =
interval === 'Weekly'
? monthUtils.currentWeek(firstDayOfWeekIdx)
: monthUtils[currentDate]();
const earliestInterval =
interval === 'Weekly'
? monthUtils.weekFromDate(
d.parseISO(fromDateRepr(trans.date || monthUtils.currentDay())),
firstDayOfWeekIdx,
)
: monthUtils[fromDate](
d.parseISO(fromDateRepr(trans.date || monthUtils.currentDay())),
);
const allIntervals =
interval === 'Weekly'
? monthUtils.weekRangeInclusive(
earliestInterval,
currentInterval,
firstDayOfWeekIdx,
)
: monthUtils[
ReportOptions.intervalRange.get(interval) || 'rangeInclusive'
](earliestInterval, currentInterval);
const allIntervalsMap = allIntervals
.map((inter: string) => ({
name: inter,
pretty: monthUtils.format(
inter,
ReportOptions.intervalFormat.get(interval),
ReportOptions.intervalFormat.get(interval) || '',
),
}))
.reverse();
setAllIntervals(allInter);
setAllIntervals(allIntervalsMap);
if (!isDateStatic) {
const [dateStart, dateEnd] = getLiveRange(
@@ -162,19 +216,32 @@ export function CustomReport() {
}
}
run();
}, [interval]);
}, [
interval,
dateRange,
firstDayOfWeekIdx,
isDateStatic,
onApplyFilter,
report.conditions,
]);
useEffect(() => {
const rangeProps =
interval === 'Weekly'
? [startDate, endDate, firstDayOfWeekIdx]
: [startDate, endDate];
setIntervals(
monthUtils[ReportOptions.intervalRange.get(interval)](...rangeProps),
);
}, [interval, startDate, endDate]);
const [start, end] = [startDate, endDate];
if (interval === 'Weekly') {
setIntervals(
monthUtils.weekRangeInclusive(start, end, firstDayOfWeekIdx),
);
} else {
setIntervals(
monthUtils[
ReportOptions.intervalRange.get(interval) || 'rangeInclusive'
](start, end),
);
}
}, [interval, startDate, endDate, firstDayOfWeekIdx]);
const balanceTypeOp = ReportOptions.balanceTypeMap.get(balanceType);
const balanceTypeOp =
ReportOptions.balanceTypeMap.get(balanceType) || 'totalDebts';
const payees = usePayees();
const accounts = useAccounts();
@@ -198,19 +265,15 @@ export function CustomReport() {
startDate,
endDate,
interval,
groupBy,
balanceType,
balanceTypeOp,
categories,
selectedCategories,
payees,
accounts,
filters,
conditionsOp,
showEmpty,
showOffBudget,
showHiddenCategories,
showUncategorized,
graphType,
firstDayOfWeekIdx,
]);
@@ -241,7 +304,7 @@ export function CustomReport() {
endDate,
interval,
groupBy,
balanceType,
balanceTypeOp,
categories,
selectedCategories,
payees,
@@ -258,8 +321,11 @@ export function CustomReport() {
const graphData = useReport('default', getGraphData);
const groupedData = useReport('grouped', getGroupData);
const data = { ...graphData, groupedData };
const customReportItems = {
const data: GroupedEntity = { ...graphData, groupedData } as GroupedEntity;
const customReportItems: CustomReportEntity = {
id: '',
name: '',
startDate,
endDate,
isDateStatic,
@@ -276,84 +342,104 @@ export function CustomReport() {
graphType,
conditions: filters,
conditionsOp,
data: {},
};
const [scrollWidth, setScrollWidth] = useState(0);
const [, setScrollWidth] = useState(0);
if (!allIntervals || !data) {
return null;
}
const defaultModeItems = (graph, item) => {
const defaultModeItems = (graph: string, item: string) => {
const chooseGraph = graph || graphType;
const newGraph = disabledList.modeGraphsMap.get(item).includes(chooseGraph)
const newGraph = (disabledList.modeGraphsMap.get(item) || []).includes(
chooseGraph,
)
? defaultsList.modeGraphsMap.get(item)
: chooseGraph;
if (disabledList.modeGraphsMap.get(item).includes(graphType)) {
if ((disabledList.modeGraphsMap.get(item) || []).includes(graphType)) {
setSessionReport('graphType', newGraph);
setGraphType(newGraph);
}
if (disabledList.graphSplitMap.get(item).get(newGraph).includes(groupBy)) {
const cond = defaultsList.graphSplitMap.get(item).get(newGraph);
if (
(disabledGraphList(item, newGraph, 'disabledSplit') || []).includes(
groupBy,
)
) {
const cond = defaultsGraphList(item, newGraph, 'defaultSplit');
setSessionReport('groupBy', cond);
setGroupBy(cond);
}
if (
disabledList.graphTypeMap.get(item).get(newGraph).includes(balanceType)
(disabledGraphList(item, newGraph, 'disabledType') || []).includes(
balanceType,
)
) {
const cond = defaultsList.graphTypeMap.get(item).get(newGraph);
const cond = defaultsGraphList(item, newGraph, 'defaultType');
setSessionReport('balanceType', cond);
setBalanceType(cond);
}
};
const defaultItems = item => {
const defaultItems = (item: string) => {
const chooseGraph = ReportOptions.groupBy.includes(item) ? graphType : item;
if (
disabledList.graphSplitMap.get(mode).get(chooseGraph).includes(groupBy)
(disabledGraphList(mode, chooseGraph, 'disabledSplit') || []).includes(
groupBy,
)
) {
const cond = defaultsList.graphSplitMap.get(mode).get(chooseGraph);
const cond = defaultsGraphList(mode, chooseGraph, 'defaultSplit');
setSessionReport('groupBy', cond);
setGroupBy(cond);
}
if (
disabledList.graphTypeMap.get(mode).get(chooseGraph).includes(balanceType)
(disabledGraphList(mode, chooseGraph, 'disabledType') || []).includes(
balanceType,
)
) {
const cond = defaultsList.graphTypeMap.get(mode).get(chooseGraph);
const cond = defaultsGraphList(mode, chooseGraph, 'defaultType');
setSessionReport('balanceType', cond);
setBalanceType(cond);
}
};
const disabledItems = type => {
const isItemDisabled = (type: string) => {
switch (type) {
case 'ShowLegend': {
if (disabledLegendLabel(mode, graphType, 'disableLegend')) {
setViewLegendPref(false);
}
return disabledLegendLabel(mode, graphType, 'disableLegend') || false;
}
case 'ShowLabels': {
if (disabledLegendLabel(mode, graphType, 'disableLabel')) {
setViewLabelsPref(false);
}
return disabledLegendLabel(mode, graphType, 'disableLabel') || false;
}
default:
return (
(disabledList.modeGraphsMap.get(mode) || []).includes(type) || false
);
}
};
const disabledItems = (type: string) => {
switch (type) {
case 'split':
return disabledList.graphSplitMap.get(mode).get(graphType);
return disabledGraphList(mode, graphType, 'disabledSplit') || [];
case 'type':
return graphType === 'BarGraph' && groupBy === 'Interval'
? []
: disabledList.graphTypeMap.get(mode).get(graphType);
case 'ShowLegend': {
if (disabledList.graphLegendMap.get(mode).get(graphType)) {
setViewLegendPref(false);
}
return disabledList.graphLegendMap.get(mode).get(graphType);
}
case 'ShowLabels': {
if (disabledList.graphLabelsMap.get(mode).get(graphType)) {
setViewLabelsPref(false);
}
return disabledList.graphLabelsMap.get(mode).get(graphType);
}
: disabledGraphList(mode, graphType, 'disabledType') || [];
default:
return disabledList.modeGraphsMap.get(mode).includes(type);
return [];
}
};
const onChangeDates = (dateStart, dateEnd) => {
const onChangeDates = (dateStart: string, dateEnd: string) => {
setSessionReport('startDate', dateStart);
setSessionReport('endDate', dateEnd);
setStartDate(dateStart);
@@ -361,9 +447,9 @@ export function CustomReport() {
onReportChange({ type: 'modify' });
};
const onChangeViews = (viewType, status) => {
const onChangeViews = (viewType: string) => {
if (viewType === 'viewLegend') {
setViewLegendPref(status ?? !viewLegend);
setViewLegendPref(!viewLegend);
}
if (viewType === 'viewSummary') {
setViewSummaryPref(!viewSummary);
@@ -373,10 +459,12 @@ export function CustomReport() {
}
};
const setReportData = input => {
const selectAll = [];
const setReportData = (input: CustomReportEntity) => {
const selectAll: CategoryEntity[] = [];
categories.grouped.map(categoryGroup =>
categoryGroup.categories.map(category => selectAll.push(category)),
(categoryGroup.categories || []).map(category =>
selectAll.push(category),
),
);
setStartDate(input.startDate);
@@ -391,19 +479,20 @@ export function CustomReport() {
setShowOffBudget(input.showOffBudget);
setShowHiddenCategories(input.showHiddenCategories);
setShowUncategorized(input.showUncategorized);
setSelectedCategories(input.selectedCategories ?? selectAll);
setSelectedCategories(input.selectedCategories || selectAll);
setGraphType(input.graphType);
onApplyFilter(null);
input.conditions.forEach(condition => onApplyFilter(condition));
(input.conditions || []).forEach(condition => onApplyFilter(condition));
onCondOpChange(input.conditionsOp);
};
const onChangeAppliedFilter = (filter, changedElement) => {
onReportChange({ type: 'modify' });
return changedElement(filter);
};
const onReportChange = ({ savedReport, type }) => {
const onReportChange = ({
savedReport,
type,
}: {
savedReport?: CustomReportEntity;
type: string;
}) => {
switch (type) {
case 'add-update':
setSessionReport('savedStatus', 'saved');
@@ -411,7 +500,7 @@ export function CustomReport() {
setReport(savedReport);
break;
case 'rename':
setReport({ ...report, name: savedReport.name });
setReport({ ...report, name: savedReport?.name || '' });
break;
case 'modify':
if (report.name) {
@@ -434,7 +523,7 @@ export function CustomReport() {
setSessionReport('savedStatus', 'saved');
setSavedStatus('saved');
setReport(savedReport);
setReportData(savedReport);
setReportData(savedReport || report);
break;
default:
}
@@ -454,7 +543,24 @@ export function CustomReport() {
flexShrink: 0,
}}
>
<Header title="Custom Report:" />
<View
style={{
padding: 10,
paddingTop: 0,
flexShrink: 0,
}}
>
<Link
variant="button"
type="bare"
to="/reports"
style={{ marginBottom: '15', alignSelf: 'flex-start' }}
>
<SvgArrowLeft width={10} height={10} style={{ marginRight: 5 }} />{' '}
Back
</Link>
<View style={styles.veryLargeText}>Custom Report:</View>
</View>
<Text
style={{
...styles.veryLargeText,
@@ -521,17 +627,20 @@ export function CustomReport() {
onApplyFilter={onApplyFilter}
onChangeViews={onChangeViews}
onReportChange={onReportChange}
disabledItems={disabledItems}
isItemDisabled={isItemDisabled}
defaultItems={defaultItems}
/>
)}
{filters && filters.length > 0 && (
<View
style={{ marginBottom: 10, marginLeft: 5, flexShrink: 0 }}
spacing={2}
direction="row"
justify="flex-start"
align="flex-start"
style={{
marginBottom: 10,
marginLeft: 5,
flexShrink: 0,
flexDirection: 'row',
alignItems: 'flex-start',
justifyContent: 'flex-start',
}}
>
<AppliedFilters
filters={filters}
@@ -548,13 +657,14 @@ export function CustomReport() {
'conditions',
filters.filter(f => f !== deletedFilter),
);
onChangeAppliedFilter(deletedFilter, onDeleteFilter);
onDeleteFilter(deletedFilter);
onReportChange({ type: 'modify' });
}}
conditionsOp={conditionsOp}
onCondOpChange={filter =>
onChangeAppliedFilter(filter, onCondOpChange)
}
onUpdateChange={onReportChange}
onCondOpChange={co => {
onCondOpChange(co);
onReportChange({ type: 'modify' });
}}
/>
</View>
)}
@@ -615,8 +725,6 @@ export function CustomReport() {
balanceType={balanceType}
groupBy={groupBy}
interval={interval}
showEmpty={showEmpty}
scrollWidth={scrollWidth}
setScrollWidth={setScrollWidth}
viewLabels={viewLabels}
compact={false}

View File

@@ -143,13 +143,13 @@ export function createCustomSpreadsheet({
});
}
const rangeProps =
const intervals =
interval === 'Weekly'
? [startDate, endDate, firstDayOfWeekIdx]
: [startDate, endDate];
const intervals = monthUtils[ReportOptions.intervalRange.get(interval)](
...rangeProps,
);
? monthUtils.weekRangeInclusive(startDate, endDate, firstDayOfWeekIdx)
: monthUtils[ReportOptions.intervalRange.get(interval)](
startDate,
endDate,
);
let totalAssets = 0;
let totalDebts = 0;

View File

@@ -92,13 +92,13 @@ export function createGroupedSpreadsheet({
});
}
const rangeProps =
const intervals =
interval === 'Weekly'
? [startDate, endDate, firstDayOfWeekIdx]
: [startDate, endDate];
const intervals = monthUtils[ReportOptions.intervalRange.get(interval)](
...rangeProps,
);
? monthUtils.weekRangeInclusive(startDate, endDate, firstDayOfWeekIdx)
: monthUtils[ReportOptions.intervalRange.get(interval)](
startDate,
endDate,
);
const groupedData: DataEntity[] = categoryGroup.map(
group => {

View File

@@ -1,7 +1,11 @@
// @ts-strict-ignore
import { useCallback, useMemo, useState } from 'react';
export function useFilters<T>(initialFilters: T[] = []) {
import { type RuleConditionEntity } from 'loot-core/types/models/rule';
export function useFilters<T extends RuleConditionEntity>(
initialFilters: T[] = [],
) {
const [filters, setFilters] = useState<T[]>(initialFilters);
const [conditionsOp, setConditionsOp] = useState<'and' | 'or'>('and');
const [saved, setSaved] = useState<T[]>(null);

View File

@@ -60,7 +60,7 @@ export interface SpendingEntity {
export interface GroupedEntity {
data?: DataEntity[];
intervalData: DataEntity[];
groupedData?: DataEntity[];
groupedData?: DataEntity[] | null;
legend?: LegendEntity[];
startDate?: string;
endDate?: string;

View File

@@ -0,0 +1,6 @@
---
category: Maintenance
authors: [carkom]
---
Custom reports: convert final jsx files to typescript.