mirror of
https://github.com/actualbudget/actual.git
synced 2026-04-28 10:33:02 -05:00
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:
@@ -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 = {
|
||||
|
||||
@@ -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])}
|
||||
/>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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];
|
||||
}
|
||||
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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),
|
||||
);
|
||||
|
||||
@@ -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),
|
||||
);
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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';
|
||||
}
|
||||
|
||||
6
upcoming-release-notes/2466.md
Normal file
6
upcoming-release-notes/2466.md
Normal 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
|
||||
Reference in New Issue
Block a user