diff --git a/packages/desktop-client/src/components/autocomplete/CategoryAutocomplete.tsx b/packages/desktop-client/src/components/autocomplete/CategoryAutocomplete.tsx index b9b418a54a2729673df667eef7cd034b0da1a226..3f1d16cdade799a327cfb468db50bba287b5d73a 100644 --- a/packages/desktop-client/src/components/autocomplete/CategoryAutocomplete.tsx +++ b/packages/desktop-client/src/components/autocomplete/CategoryAutocomplete.tsx @@ -36,7 +36,7 @@ type CategoryAutocompleteItem = CategoryEntity & { group?: CategoryGroupEntity; }; -export type CategoryListProps = { +type CategoryListProps = { items: CategoryAutocompleteItem[]; getItemProps?: (arg: { item: CategoryAutocompleteItem; diff --git a/packages/desktop-client/src/components/reports/CategorySelector.tsx b/packages/desktop-client/src/components/reports/CategorySelector.tsx index d9639de36214dc6f3c83cd67e8f2dbaf2b107e10..317ca37275327a5b27f4bd6a350a0183667037f6 100644 --- a/packages/desktop-client/src/components/reports/CategorySelector.tsx +++ b/packages/desktop-client/src/components/reports/CategorySelector.tsx @@ -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; }; diff --git a/packages/desktop-client/src/components/reports/Overview.jsx b/packages/desktop-client/src/components/reports/Overview.tsx similarity index 94% rename from packages/desktop-client/src/components/reports/Overview.jsx rename to packages/desktop-client/src/components/reports/Overview.tsx index 06ecef0c81535bcf3f446011d8fb08cd50387378..550e039c63f831d714199408876d56325793de46 100644 --- a/packages/desktop-client/src/components/reports/Overview.jsx +++ b/packages/desktop-client/src/components/reports/Overview.tsx @@ -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 && ( diff --git a/packages/desktop-client/src/components/reports/ReportOptions.ts b/packages/desktop-client/src/components/reports/ReportOptions.ts index acad7f742d7921f41fa60dbc938d3923ce7f9c71..e8c7bd8becf915c40fc00e470fdc52c76ecb38b1 100644 --- a/packages/desktop-client/src/components/reports/ReportOptions.ts +++ b/packages/desktop-client/src/components/reports/ReportOptions.ts @@ -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 = { diff --git a/packages/desktop-client/src/components/reports/ReportRouter.jsx b/packages/desktop-client/src/components/reports/ReportRouter.tsx similarity index 100% rename from packages/desktop-client/src/components/reports/ReportRouter.jsx rename to packages/desktop-client/src/components/reports/ReportRouter.tsx diff --git a/packages/desktop-client/src/components/reports/ReportSidebar.jsx b/packages/desktop-client/src/components/reports/ReportSidebar.tsx similarity index 84% rename from packages/desktop-client/src/components/reports/ReportSidebar.jsx rename to packages/desktop-client/src/components/reports/ReportSidebar.tsx index 23f949a4a9b253d757e1cae53bd379f03491e974..33fbe5c128c38d377ec72c6933922eba3518a173 100644 --- a/packages/desktop-client/src/components/reports/ReportSidebar.jsx +++ b/packages/desktop-client/src/components/reports/ReportSidebar.tsx @@ -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' }); diff --git a/packages/desktop-client/src/components/reports/ReportTopbar.jsx b/packages/desktop-client/src/components/reports/ReportTopbar.tsx similarity index 77% rename from packages/desktop-client/src/components/reports/ReportTopbar.jsx rename to packages/desktop-client/src/components/reports/ReportTopbar.tsx index 0bb9c45f99e1633b2a1111c23ca9cbf030d094f7..042b781a1188e243b13fc6422df1336c95af4f96 100644 --- a/packages/desktop-client/src/components/reports/ReportTopbar.jsx +++ b/packages/desktop-client/src/components/reports/ReportTopbar.tsx @@ -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 diff --git a/packages/desktop-client/src/components/reports/disabledList.ts b/packages/desktop-client/src/components/reports/disabledList.ts index b641a59f28884554a70b3029ea9ac5e5826cae49..ad3caadb6e95f94d0c2590961e969b704c662ef7 100644 --- a/packages/desktop-client/src/components/reports/disabledList.ts +++ b/packages/desktop-client/src/components/reports/disabledList.ts @@ -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]), ), diff --git a/packages/desktop-client/src/components/reports/getLiveRange.ts b/packages/desktop-client/src/components/reports/getLiveRange.ts index a7030ebe680edbc7ca32708e6e02b0d71ec28143..437892f6fe5423c35770a58240bab50321f35073 100644 --- a/packages/desktop-client/src/components/reports/getLiveRange.ts +++ b/packages/desktop-client/src/components/reports/getLiveRange.ts @@ -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': diff --git a/packages/desktop-client/src/components/reports/reportRanges.ts b/packages/desktop-client/src/components/reports/reportRanges.ts index 0acf4d42b2866ad60903cd5ddd240f71552cf4b3..720954f597e780455d8b908871b1fc5ebdab1e11 100644 --- a/packages/desktop-client/src/components/reports/reportRanges.ts +++ b/packages/desktop-client/src/components/reports/reportRanges.ts @@ -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': diff --git a/packages/desktop-client/src/components/reports/reports/CustomReport.jsx b/packages/desktop-client/src/components/reports/reports/CustomReport.tsx similarity index 70% rename from packages/desktop-client/src/components/reports/reports/CustomReport.jsx rename to packages/desktop-client/src/components/reports/reports/CustomReport.tsx index cc375de1dbcf5b66577d9128b32a832af77978c5..53320dd83fb5121f16a97aee84b271314528712c 100644 --- a/packages/desktop-client/src/components/reports/reports/CustomReport.jsx +++ b/packages/desktop-client/src/components/reports/reports/CustomReport.tsx @@ -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 fromDate = + interval === 'Weekly' + ? 'dayFromDate' + : (((ReportOptions.intervalMap.get(interval) || 'Day').toLowerCase() + + 'FromDate') as 'dayFromDate' | 'monthFromDate' | 'yearFromDate'); - const rangeProps = + const currentDate = interval === 'Weekly' - ? [earliestInterval, currentInterval, firstDayOfWeekIdx] - : [earliestInterval, currentInterval]; - const allInter = monthUtils[ReportOptions.intervalRange.get(interval)]( - ...rangeProps, - ) - .map(inter => ({ + ? '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 'split': - return disabledList.graphSplitMap.get(mode).get(graphType); - case 'type': - return graphType === 'BarGraph' && groupBy === 'Interval' - ? [] - : disabledList.graphTypeMap.get(mode).get(graphType); case 'ShowLegend': { - if (disabledList.graphLegendMap.get(mode).get(graphType)) { + if (disabledLegendLabel(mode, graphType, 'disableLegend')) { setViewLegendPref(false); } - return disabledList.graphLegendMap.get(mode).get(graphType); + return disabledLegendLabel(mode, graphType, 'disableLegend') || false; } case 'ShowLabels': { - if (disabledList.graphLabelsMap.get(mode).get(graphType)) { + if (disabledLegendLabel(mode, graphType, 'disableLabel')) { setViewLabelsPref(false); } - return disabledList.graphLabelsMap.get(mode).get(graphType); + return disabledLegendLabel(mode, graphType, 'disableLabel') || false; } default: - return disabledList.modeGraphsMap.get(mode).includes(type); + return ( + (disabledList.modeGraphsMap.get(mode) || []).includes(type) || false + ); } }; - const onChangeDates = (dateStart, dateEnd) => { + const disabledItems = (type: string) => { + switch (type) { + case 'split': + return disabledGraphList(mode, graphType, 'disabledSplit') || []; + case 'type': + return graphType === 'BarGraph' && groupBy === 'Interval' + ? [] + : disabledGraphList(mode, graphType, 'disabledType') || []; + default: + return []; + } + }; + + 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} diff --git a/packages/desktop-client/src/components/reports/spreadsheets/custom-spreadsheet.ts b/packages/desktop-client/src/components/reports/spreadsheets/custom-spreadsheet.ts index e40e32b659172279b1ed8b67393dc11a38ef33b6..5251bafc1abedcd1de638b78eb69f9d6d51aee1f 100644 --- a/packages/desktop-client/src/components/reports/spreadsheets/custom-spreadsheet.ts +++ b/packages/desktop-client/src/components/reports/spreadsheets/custom-spreadsheet.ts @@ -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; diff --git a/packages/desktop-client/src/components/reports/spreadsheets/grouped-spreadsheet.ts b/packages/desktop-client/src/components/reports/spreadsheets/grouped-spreadsheet.ts index 591cb81cfb48006a94fe234123ae134a5906fbfb..b273ccf07f66b397f23d1dc19d694eb025ecd7dd 100644 --- a/packages/desktop-client/src/components/reports/spreadsheets/grouped-spreadsheet.ts +++ b/packages/desktop-client/src/components/reports/spreadsheets/grouped-spreadsheet.ts @@ -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 => { diff --git a/packages/desktop-client/src/hooks/useFilters.ts b/packages/desktop-client/src/hooks/useFilters.ts index 4bb0e98481f78ec8f3a54e1959fe07c686a870a5..cd8fcaed8688a75b0d90c1b36f8f2f767f808124 100644 --- a/packages/desktop-client/src/hooks/useFilters.ts +++ b/packages/desktop-client/src/hooks/useFilters.ts @@ -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); diff --git a/packages/loot-core/src/types/models/reports.d.ts b/packages/loot-core/src/types/models/reports.d.ts index 3379aeac32caa12a6351e18b598cd3f5c1a245ed..eb9fe981adb9d85a0cb66b4734d3e288c7e53e42 100644 --- a/packages/loot-core/src/types/models/reports.d.ts +++ b/packages/loot-core/src/types/models/reports.d.ts @@ -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; diff --git a/upcoming-release-notes/2707.md b/upcoming-release-notes/2707.md new file mode 100644 index 0000000000000000000000000000000000000000..57939dac3ea4c22472d1377e8ced4ac48cbd9905 --- /dev/null +++ b/upcoming-release-notes/2707.md @@ -0,0 +1,6 @@ +--- +category: Maintenance +authors: [carkom] +--- + +Custom reports: convert final jsx files to typescript.