From 308f8339aebf2fedd2e8fc1cb7ba110b9e55c303 Mon Sep 17 00:00:00 2001 From: Neil <55785687+carkom@users.noreply.github.com> Date: Tue, 2 Apr 2024 20:29:20 +0100 Subject: [PATCH] Add yearly to custom reports (#2466) * Button changes and time filters * rename on dashboard * notes * fix time filters * Sort Categories * Page title * category sort order * move button * featureflag * Highlight report name * sankey fix * VRT * remove doubled element * adjust to match master * add Year * notes * lint fix * update names * IntervalsUpdates * fixing bugs * ts updates * lint fix * merge fixes * notes * simplify lookups --- .../src/components/reports/ReportOptions.ts | 41 ++++++++++--- .../src/components/reports/ReportSidebar.jsx | 28 +++++---- .../src/components/reports/ReportSummary.tsx | 15 ++++- .../src/components/reports/reportRanges.ts | 32 ++-------- .../reports/reports/CustomReport.jsx | 61 ++++++++++++------- .../spreadsheets/custom-spreadsheet.ts | 13 ++-- .../spreadsheets/grouped-spreadsheet.ts | 9 ++- .../reports/spreadsheets/makeQuery.ts | 5 +- packages/loot-core/src/shared/months.ts | 47 ++++++++++++++ upcoming-release-notes/2466.md | 6 ++ 10 files changed, 171 insertions(+), 86 deletions(-) create mode 100644 upcoming-release-notes/2466.md diff --git a/packages/desktop-client/src/components/reports/ReportOptions.ts b/packages/desktop-client/src/components/reports/ReportOptions.ts index 7a7c717ca..cf6e8becd 100644 --- a/packages/desktop-client/src/components/reports/ReportOptions.ts +++ b/packages/desktop-client/src/components/reports/ReportOptions.ts @@ -45,22 +45,37 @@ const groupByOptions = [ ]; const dateRangeOptions = [ - { description: 'This month', name: 0 }, - { description: 'Last month', name: 1 }, - { description: 'Last 3 months', name: 2 }, - { description: 'Last 6 months', name: 5 }, - { description: 'Last 12 months', name: 11 }, - { description: 'Year to date', name: 'yearToDate' }, - { description: 'Last year', name: 'lastYear' }, - { description: 'All time', name: 'allMonths' }, + { description: 'This month', name: 0, Yearly: false, Monthly: true }, + { description: 'Last month', name: 1, Yearly: false, Monthly: true }, + { description: 'Last 3 months', name: 2, Yearly: false, Monthly: true }, + { description: 'Last 6 months', name: 5, Yearly: false, Monthly: true }, + { description: 'Last 12 months', name: 11, Yearly: false, Monthly: true }, + { + description: 'Year to date', + name: 'yearToDate', + Yearly: true, + Monthly: true, + }, + { description: 'Last year', name: 'lastYear', Yearly: true, Monthly: true }, + { description: 'All time', name: 'allMonths', Yearly: true, Monthly: true }, ]; const intervalOptions = [ //{ value: 1, description: 'Daily', name: 'Day'}, //{ value: 2, description: 'Weekly', name: 'Week'}, //{ value: 3, description: 'Fortnightly', name: 3}, - { value: 4, description: 'Monthly', name: 'Month' }, - { value: 5, description: 'Yearly', name: 'Year' }, + { + description: 'Monthly', + name: 'Month', + format: 'MMMM, yyyy', + range: 'rangeInclusive', + }, + { + description: 'Yearly', + name: 'Year', + format: 'yyyy', + range: 'yearRangeInclusive', + }, ]; export const ReportOptions = { @@ -77,6 +92,12 @@ export const ReportOptions = { intervalMap: new Map( 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]), + ), }; export type QueryDataEntity = { diff --git a/packages/desktop-client/src/components/reports/ReportSidebar.jsx b/packages/desktop-client/src/components/reports/ReportSidebar.jsx index 4e4b3f84f..e916183c7 100644 --- a/packages/desktop-client/src/components/reports/ReportSidebar.jsx +++ b/packages/desktop-client/src/components/reports/ReportSidebar.jsx @@ -77,7 +77,6 @@ export function ReportSidebar({ [dateStart, dateEnd] = getSpecificRange( ReportOptions.dateRangeMap.get(cond), cond === 'Last month' ? 0 : null, - customReportItems.interval, ); onChangeDates(dateStart, dateEnd); } @@ -209,16 +208,20 @@ export function ReportSidebar({ onChange={e => { setInterval(e); onReportChange({ type: 'modify' }); + if ( + ReportOptions.dateRange + .filter(int => !int[e]) + .map(int => int.description) + .includes(customReportItems.dateRange) + ) { + onSelectRange('Year to date'); + } }} options={ReportOptions.interval.map(option => [ option.description, option.description, ])} - disabledKeys={ - customReportItems.mode === 'time' - ? ['Monthly', 'Yearly'] - : ['Yearly'] - } + disabledKeys={[]} /> </View> <View @@ -354,11 +357,10 @@ export function ReportSidebar({ onChange={e => { onSelectRange(e); }} - options={ReportOptions.dateRange.map(option => [ - option.description, - option.description, - ])} - line={dateRangeLine} + options={ReportOptions.dateRange + .filter(f => f[customReportItems.interval]) + .map(option => [option.description, option.description])} + line={customReportItems.interval === 'Monthly' && dateRangeLine} /> </View> ) : ( @@ -387,7 +389,7 @@ export function ReportSidebar({ value={customReportItems.startDate} defaultLabel={monthUtils.format( customReportItems.startDate, - 'MMMM, yyyy', + ReportOptions.intervalFormat.get(customReportItems.interval), )} options={allIntervals.map(({ name, pretty }) => [name, pretty])} /> @@ -416,7 +418,7 @@ export function ReportSidebar({ value={customReportItems.endDate} defaultLabel={monthUtils.format( customReportItems.endDate, - 'MMMM, yyyy', + ReportOptions.intervalFormat.get(customReportItems.interval), )} options={allIntervals.map(({ name, pretty }) => [name, pretty])} /> diff --git a/packages/desktop-client/src/components/reports/ReportSummary.tsx b/packages/desktop-client/src/components/reports/ReportSummary.tsx index 5653a19ce..48f570c6b 100644 --- a/packages/desktop-client/src/components/reports/ReportSummary.tsx +++ b/packages/desktop-client/src/components/reports/ReportSummary.tsx @@ -14,6 +14,8 @@ import { Text } from '../common/Text'; import { View } from '../common/View'; import { PrivacyFilter } from '../PrivacyFilter'; +import { ReportOptions } from './ReportOptions'; + type ReportSummaryProps = { startDate: string; endDate: string; @@ -59,8 +61,15 @@ export function ReportSummary({ fontWeight: 600, }} > - {monthUtils.format(startDate, 'MMM yyyy')} -{' '} - {monthUtils.format(endDate, 'MMM yyyy')} + {monthUtils.format( + startDate, + ReportOptions.intervalFormat.get(interval), + )}{' '} + -{' '} + {monthUtils.format( + endDate, + ReportOptions.intervalFormat.get(interval), + )} </Text> </View> <View @@ -136,7 +145,7 @@ export function ReportSummary({ </PrivacyFilter> </Text> <Text style={{ fontWeight: 600 }}> - Per {interval === 'Monthly' ? 'month' : 'year'} + Per {ReportOptions.intervalMap.get(interval).toLowerCase()} </Text> </View> </View> diff --git a/packages/desktop-client/src/components/reports/reportRanges.ts b/packages/desktop-client/src/components/reports/reportRanges.ts index ae0e1a33b..52a79c1d2 100644 --- a/packages/desktop-client/src/components/reports/reportRanges.ts +++ b/packages/desktop-client/src/components/reports/reportRanges.ts @@ -115,33 +115,13 @@ function boundedRange( return [start, end]; } -export function getSpecificRange( - offset: number, - addNumber: number, - interval: string, -) { +export function getSpecificRange(offset: number, addNumber: number) { const currentDay = monthUtils.currentDay(); - let currInterval; - let dateStart; - let dateEnd; - switch (interval) { - case 'Monthly': - currInterval = monthUtils.monthFromDate(currentDay); - dateStart = monthUtils.subMonths(currInterval, offset); - dateEnd = monthUtils.addMonths( - dateStart, - addNumber === null ? offset : addNumber, - ); - break; - default: - currInterval = currentDay; - dateStart = monthUtils.subDays(currInterval, offset); - dateEnd = monthUtils.addDays( - dateStart, - addNumber === null ? offset : addNumber, - ); - break; - } + const dateStart = monthUtils.subMonths(currentDay, offset) + '-01'; + const dateEnd = monthUtils.getMonthEnd( + monthUtils.addMonths(dateStart, addNumber === null ? offset : addNumber) + + '-01', + ); return [dateStart, dateEnd]; } diff --git a/packages/desktop-client/src/components/reports/reports/CustomReport.jsx b/packages/desktop-client/src/components/reports/reports/CustomReport.jsx index a8c0691a1..c7e89460b 100644 --- a/packages/desktop-client/src/components/reports/reports/CustomReport.jsx +++ b/packages/desktop-client/src/components/reports/reports/CustomReport.jsx @@ -81,14 +81,29 @@ export function CustomReport() { const [dateRange, setDateRange] = useState(loadReport.dateRange); const [dataCheck, setDataCheck] = useState(false); - const dateRangeLine = ReportOptions.dateRange.length - 3; + const dateRangeLine = + ReportOptions.dateRange.filter(f => f[interval]).length - 3; + const [intervals, setIntervals] = useState( + monthUtils.rangeInclusive(startDate, endDate), + ); const [earliestTransaction, setEarliestTransaction] = useState(''); const [report, setReport] = useState(loadReport); const [savedStatus, setSavedStatus] = useState( location.state ? (location.state.report ? 'saved' : 'new') : 'new', ); - const intervals = monthUtils.rangeInclusive(startDate, endDate); + + useEffect(() => { + const format = + ReportOptions.intervalMap.get(interval).toLowerCase() + 'FromDate'; + + const dateStart = monthUtils[format](startDate); + const dateEnd = monthUtils[format](endDate); + + setIntervals( + monthUtils[ReportOptions.intervalRange.get(interval)](dateStart, dateEnd), + ); + }, [interval, startDate, endDate]); useEffect(() => { if (selectedCategories === undefined && categories.list.length !== 0) { @@ -101,31 +116,31 @@ export function CustomReport() { report.conditions.forEach(condition => onApplyFilter(condition)); const trans = await send('get-earliest-transaction'); setEarliestTransaction(trans ? trans.date : monthUtils.currentDay()); - const currentMonth = monthUtils.currentMonth(); - let earliestMonth = trans - ? monthUtils.monthFromDate(d.parseISO(fromDateRepr(trans.date))) - : currentMonth; - - // Make sure the month selects are at least populates with a - // year's worth of months. We can undo this when we have fancier - // date selects. - const yearAgo = monthUtils.subMonths(monthUtils.currentMonth(), 12); - if (earliestMonth > yearAgo) { - earliestMonth = yearAgo; - } - - const allInter = monthUtils - .rangeInclusive(earliestMonth, monthUtils.currentMonth()) - .map(month => ({ - name: month, - pretty: monthUtils.format(month, 'MMMM, yyyy'), + 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 allInter = monthUtils[ReportOptions.intervalRange.get(interval)]( + earliestInterval, + currentInterval, + ) + .map(inter => ({ + name: inter, + pretty: monthUtils.format( + inter, + ReportOptions.intervalFormat.get(interval), + ), })) .reverse(); setAllIntervals(allInter); } run(); - }, []); + }, [interval]); const balanceTypeOp = ReportOptions.balanceTypeMap.get(balanceType); const payees = usePayees(); @@ -149,8 +164,8 @@ export function CustomReport() { }, [ startDate, endDate, - groupBy, interval, + groupBy, balanceType, categories, selectedCategories, @@ -189,8 +204,8 @@ export function CustomReport() { }, [ startDate, endDate, - groupBy, interval, + groupBy, balanceType, categories, selectedCategories, 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 82de8084a..dc5a601e6 100644 --- a/packages/desktop-client/src/components/reports/spreadsheets/custom-spreadsheet.ts +++ b/packages/desktop-client/src/components/reports/spreadsheets/custom-spreadsheet.ts @@ -18,7 +18,11 @@ import { type GroupedEntity, } from 'loot-core/src/types/models/reports'; -import { categoryLists, groupBySelections } from '../ReportOptions'; +import { + categoryLists, + groupBySelections, + ReportOptions, +} from '../ReportOptions'; import { calculateLegend } from './calculateLegend'; import { filterEmptyRows } from './filterEmptyRows'; @@ -123,10 +127,9 @@ export function createCustomSpreadsheet({ ).then(({ data }) => data), ]); - const rangeInc = - interval === 'Monthly' ? 'rangeInclusive' : 'yearRangeInclusive'; - const format = interval === 'Monthly' ? 'monthFromDate' : 'yearFromDate'; - const intervals = monthUtils[rangeInc]( + const format = + ReportOptions.intervalMap.get(interval).toLowerCase() + 'FromDate'; + const intervals = monthUtils[ReportOptions.intervalRange.get(interval)]( monthUtils[format](startDate), monthUtils[format](endDate), ); 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 822b53f1c..95cbf490d 100644 --- a/packages/desktop-client/src/components/reports/spreadsheets/grouped-spreadsheet.ts +++ b/packages/desktop-client/src/components/reports/spreadsheets/grouped-spreadsheet.ts @@ -6,7 +6,7 @@ import * as monthUtils from 'loot-core/src/shared/months'; import { integerToAmount } from 'loot-core/src/shared/util'; import { type DataEntity } from 'loot-core/src/types/models/reports'; -import { categoryLists } from '../ReportOptions'; +import { categoryLists, ReportOptions } from '../ReportOptions'; import { type createCustomSpreadsheetProps } from './custom-spreadsheet'; import { filterEmptyRows } from './filterEmptyRows'; @@ -78,10 +78,9 @@ export function createGroupedSpreadsheet({ ).then(({ data }) => data), ]); - const rangeInc = - interval === 'Monthly' ? 'rangeInclusive' : 'yearRangeInclusive'; - const format = interval === 'Monthly' ? 'monthFromDate' : 'yearFromDate'; - const intervals = monthUtils[rangeInc]( + const format = + ReportOptions.intervalMap.get(interval).toLowerCase() + 'FromDate'; + const intervals = monthUtils[ReportOptions.intervalRange.get(interval)]( monthUtils[format](startDate), monthUtils[format](endDate), ); diff --git a/packages/desktop-client/src/components/reports/spreadsheets/makeQuery.ts b/packages/desktop-client/src/components/reports/spreadsheets/makeQuery.ts index 3b33809d7..31f1e8b7f 100644 --- a/packages/desktop-client/src/components/reports/spreadsheets/makeQuery.ts +++ b/packages/desktop-client/src/components/reports/spreadsheets/makeQuery.ts @@ -1,6 +1,8 @@ import { q } from 'loot-core/src/shared/query'; import { type CategoryEntity } from 'loot-core/src/types/models'; +import { ReportOptions } from '../ReportOptions'; + export function makeQuery( name: string, startDate: string, @@ -13,7 +15,8 @@ export function makeQuery( ) { const intervalGroup = interval === 'Monthly' ? { $month: '$date' } : { $year: '$date' }; - const intervalFilter = interval === 'Monthly' ? '$month' : '$year'; + const intervalFilter = + '$' + ReportOptions.intervalMap.get(interval)?.toLowerCase() || 'month'; const query = q('transactions') //Apply Category_Selector diff --git a/packages/loot-core/src/shared/months.ts b/packages/loot-core/src/shared/months.ts index 5655104ea..488e4d72c 100644 --- a/packages/loot-core/src/shared/months.ts +++ b/packages/loot-core/src/shared/months.ts @@ -87,6 +87,10 @@ export function monthFromDate(date: DateLike): string { return d.format(_parse(date), 'yyyy-MM'); } +export function weekFromDate(date: DateLike): string { + return d.format(_parse(date), 'yyyy-ww'); +} + export function dayFromDate(date: DateLike): string { return d.format(_parse(date), 'yyyy-MM-dd'); } @@ -99,6 +103,14 @@ export function currentMonth(): string { } } +export function currentYear(): string { + if (global.IS_TESTING || Platform.isPlaywright) { + return global.currentMonth || '2017'; + } else { + return d.format(new Date(), 'yyyy'); + } +} + export function currentDate(): Date { if (global.IS_TESTING || Platform.isPlaywright) { return d.parse(currentDay(), 'yyyy-MM-dd', new Date()); @@ -127,6 +139,10 @@ export function prevMonth(month: DateLike): string { return d.format(d.subMonths(_parse(month), 1), 'yyyy-MM'); } +export function addYears(year: DateLike, n: number): string { + return d.format(d.addYears(_parse(year), n), 'yyyy'); +} + export function addMonths(month: DateLike, n: number): string { return d.format(d.addMonths(_parse(month), n), 'yyyy-MM'); } @@ -153,6 +169,10 @@ export function subMonths(month: string | Date, n: number) { return d.format(d.subMonths(_parse(month), n), 'yyyy-MM'); } +export function subYears(year: string | Date, n: number) { + return d.format(d.subYears(_parse(year), n), 'yyyy'); +} + export function addDays(day: DateLike, n: number): string { return d.format(d.addDays(_parse(day), n), 'yyyy-MM-dd'); } @@ -178,6 +198,29 @@ export function bounds(month: DateLike): { start: number; end: number } { }; } +export function _yearRange( + start: DateLike, + end: DateLike, + inclusive = false, +): string[] { + const years: string[] = []; + let year = yearFromDate(start); + while (d.isBefore(_parse(year), _parse(end))) { + years.push(year); + year = addYears(year, 1); + } + + if (inclusive) { + years.push(year); + } + + return years; +} + +export function yearRangeInclusive(start: DateLike, end: DateLike): string[] { + return _yearRange(start, end, true); +} + export function _range( start: DateLike, end: DateLike, @@ -249,6 +292,10 @@ export function getMonth(day: string): string { return day.slice(0, 7); } +export function getMonthEnd(day: string): string { + return subDays(nextMonth(day.slice(0, 7)) + '-01', 1); +} + export function getYearStart(month: string): string { return getYear(month) + '-01'; } diff --git a/upcoming-release-notes/2466.md b/upcoming-release-notes/2466.md new file mode 100644 index 000000000..17be3c75e --- /dev/null +++ b/upcoming-release-notes/2466.md @@ -0,0 +1,6 @@ +--- +category: Enhancements +authors: [carkom] +--- + +Enable "yearly" interval to custom reports. Also sets-up groudwork for adding weekly/daily in the near future -- GitLab