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
This commit is contained in:
Neil
2024-04-02 20:29:20 +01:00
committed by GitHub
parent 9697795279
commit 308f8339ae
10 changed files with 170 additions and 85 deletions

View File

@@ -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 = {

View File

@@ -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])}
/>

View File

@@ -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>

View File

@@ -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];
}

View File

@@ -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;
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;
// 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 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,

View File

@@ -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),
);

View File

@@ -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),
);

View File

@@ -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

View File

@@ -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';
}

View File

@@ -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