Add daily and weekly to custom reports interval list (#2483)

* 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

* Add Daily and Weekly

* notes

* notes

* merge fix

* TS fix

* Change week start date based on user prefs

* fix weeks

* fix averages

* remove $week

* remove date-week

* TS fixes
This commit is contained in:
Neil
2024-04-16 16:19:05 +01:00
committed by GitHub
parent 77a670bbc3
commit d9f55460dd
15 changed files with 332 additions and 48 deletions

View File

@@ -1,7 +1,6 @@
// @ts-strict-ignore
import React, { useRef } from 'react';
import * as monthUtils from 'loot-core/src/shared/months';
import { type GroupedEntity } from 'loot-core/src/types/models/reports';
import { type CSSProperties } from '../../style';
@@ -20,8 +19,6 @@ import { ReportTableTotals } from './graphs/tableGraph/ReportTableTotals';
import { ReportOptions } from './ReportOptions';
type ChooseGraphProps = {
startDate: string;
endDate: string;
data: GroupedEntity;
mode: string;
graphType: string;
@@ -34,11 +31,10 @@ type ChooseGraphProps = {
style?: CSSProperties;
showHiddenCategories?: boolean;
showOffBudget?: boolean;
intervalsCount?: number;
};
export function ChooseGraph({
startDate,
endDate,
data,
mode,
graphType,
@@ -51,8 +47,8 @@ export function ChooseGraph({
style,
showHiddenCategories,
showOffBudget,
intervalsCount,
}: ChooseGraphProps) {
const intervals: string[] = monthUtils.rangeInclusive(startDate, endDate);
const graphStyle = compact ? { ...style } : { flexGrow: 1 };
const balanceTypeOp = ReportOptions.balanceTypeMap.get(balanceType);
const groupByData =
@@ -166,7 +162,7 @@ export function ChooseGraph({
groupBy={groupBy}
data={data[groupByData]}
mode={mode}
intervalsCount={intervals.length}
intervalsCount={intervalsCount}
compact={compact}
style={rowStyle}
compactStyle={compactStyle}
@@ -177,7 +173,7 @@ export function ChooseGraph({
data={data}
mode={mode}
balanceTypeOp={balanceTypeOp}
intervalsCount={intervals.length}
intervalsCount={intervalsCount}
compact={compact}
style={rowStyle}
compactStyle={compactStyle}

View File

@@ -45,24 +45,106 @@ const groupByOptions = [
];
const dateRangeOptions = [
{ 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: 'This week',
type: 'Weeks',
name: 0,
Yearly: false,
Monthly: false,
Daily: true,
Weekly: true,
},
{
description: 'Last week',
type: 'Weeks',
name: 1,
Yearly: false,
Monthly: false,
Daily: true,
Weekly: true,
},
{
description: 'This month',
type: 'Months',
name: 0,
Yearly: false,
Monthly: true,
Daily: true,
Weekly: true,
},
{
description: 'Last month',
type: 'Months',
name: 1,
Yearly: false,
Monthly: true,
Daily: true,
Weekly: true,
},
{
description: 'Last 3 months',
type: 'Months',
name: 2,
Yearly: false,
Monthly: true,
Daily: true,
Weekly: true,
},
{
description: 'Last 6 months',
type: 'Months',
name: 5,
Yearly: false,
Monthly: true,
Daily: false,
},
{
description: 'Last 12 months',
type: 'Months',
name: 11,
Yearly: false,
Monthly: true,
Daily: false,
},
{
description: 'Year to date',
name: 'yearToDate',
Yearly: true,
Monthly: true,
Daily: true,
Weekly: true,
},
{
description: 'Last year',
name: 'lastYear',
Yearly: true,
Monthly: true,
Daily: true,
Weekly: true,
},
{
description: 'All time',
name: 'allTime',
Yearly: true,
Monthly: true,
Daily: true,
Weekly: 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'},
{
description: 'Daily',
name: 'Day',
format: 'yyyy-MM-dd',
range: 'dayRangeInclusive',
},
{
description: 'Weekly',
name: 'Week',
format: 'yyyy-MM-dd',
range: 'weekRangeInclusive',
},
//{ value: 3, description: 'Fortnightly', name: 3},
{
description: 'Monthly',
@@ -88,6 +170,9 @@ export const ReportOptions = {
dateRangeMap: new Map(
dateRangeOptions.map(item => [item.description, item.name]),
),
dateRangeType: new Map(
dateRangeOptions.map(item => [item.description, item.type]),
),
interval: intervalOptions,
intervalMap: new Map(
intervalOptions.map(item => [item.description, item.name]),

View File

@@ -11,6 +11,7 @@ import { View } from '../common/View';
import { Tooltip } from '../tooltips';
import { CategorySelector } from './CategorySelector';
import { defaultsList } from './disabledList';
import { getLiveRange } from './getLiveRange';
import { ModeButton } from './ModeButton';
import { ReportOptions } from './ReportOptions';
@@ -39,6 +40,7 @@ export function ReportSidebar({
defaultItems,
defaultModeItems,
earliestTransaction,
firstDayOfWeekIdx,
}) {
const [menuOpen, setMenuOpen] = useState(false);
const onSelectRange = cond => {
@@ -49,7 +51,9 @@ export function ReportSidebar({
);
onReportChange({ type: 'modify' });
setDateRange(cond);
onChangeDates(...getLiveRange(cond, earliestTransaction));
onChangeDates(
...getLiveRange(cond, earliestTransaction, firstDayOfWeekIdx),
);
};
const onChangeMode = cond => {
@@ -207,7 +211,7 @@ export function ReportSidebar({
.map(int => int.description)
.includes(customReportItems.dateRange)
) {
onSelectRange('Year to date');
onSelectRange(defaultsList.intervalRange.get(e));
}
}}
options={ReportOptions.interval.map(option => [
@@ -396,7 +400,7 @@ export function ReportSidebar({
options={ReportOptions.dateRange
.filter(f => f[customReportItems.interval])
.map(option => [option.description, option.description])}
line={customReportItems.interval === 'Monthly' && dateRangeLine}
line={dateRangeLine > 0 && dateRangeLine}
/>
</View>
) : (
@@ -419,6 +423,7 @@ export function ReportSidebar({
newValue,
customReportItems.endDate,
customReportItems.interval,
firstDayOfWeekIdx,
),
)
}
@@ -448,6 +453,7 @@ export function ReportSidebar({
customReportItems.startDate,
newValue,
customReportItems.interval,
firstDayOfWeekIdx,
),
)
}

View File

@@ -64,12 +64,20 @@ export function ReportSummary({
{monthUtils.format(
startDate,
ReportOptions.intervalFormat.get(interval),
)}{' '}
-{' '}
{monthUtils.format(
endDate,
ReportOptions.intervalFormat.get(interval),
)}
{monthUtils.format(
startDate,
ReportOptions.intervalFormat.get(interval),
) !==
monthUtils.format(
endDate,
ReportOptions.intervalFormat.get(interval),
) &&
' to ' +
monthUtils.format(
endDate,
ReportOptions.intervalFormat.get(interval),
)}
</Text>
</View>
<View

View File

@@ -1,3 +1,22 @@
const intervalOptions = [
{
description: 'Daily',
defaultRange: 'This month',
},
{
description: 'Weekly',
defaultRange: 'Last 3 months',
},
{
description: 'Monthly',
defaultRange: 'Last 6 months',
},
{
description: 'Yearly',
defaultRange: 'Year to date',
},
];
const totalGraphOptions = [
{
description: 'TableGraph',
@@ -123,4 +142,7 @@ export const defaultsList = {
new Map([...item.graphs].map(f => [f.description, f.defaultType])),
]),
),
intervalRange: new Map(
intervalOptions.map(item => [item.description, item.defaultRange]),
),
};

View File

@@ -3,7 +3,11 @@ import * as monthUtils from 'loot-core/src/shared/months';
import { ReportOptions } from './ReportOptions';
import { getSpecificRange, validateRange } from './reportRanges';
export function getLiveRange(cond: string, earliestTransaction: string) {
export function getLiveRange(
cond: string,
earliestTransaction: string,
firstDayOfWeekIdx?: 0 | 1 | 2 | 3 | 4 | 5 | 6,
) {
let dateStart;
let dateEnd;
const rangeName = ReportOptions.dateRangeMap.get(cond);
@@ -25,7 +29,7 @@ export function getLiveRange(cond: string, earliestTransaction: string) {
'-31',
);
break;
case 'allMonths':
case 'allTime':
dateStart = earliestTransaction;
dateEnd = monthUtils.currentDay();
break;
@@ -33,7 +37,9 @@ export function getLiveRange(cond: string, earliestTransaction: string) {
if (typeof rangeName === 'number') {
[dateStart, dateEnd] = getSpecificRange(
rangeName,
cond === 'Last month' ? 0 : null,
cond === 'Last month' || cond === 'Last week' ? 0 : null,
ReportOptions.dateRangeType.get(cond),
firstDayOfWeekIdx,
);
} else {
break;

View File

@@ -5,6 +5,7 @@ export function validateStart(
start: string,
end: string,
interval?: string,
firstDayOfWeekIdx?: 0 | 1 | 2 | 3 | 4 | 5 | 6,
) {
let addDays;
let dateStart;
@@ -35,6 +36,7 @@ export function validateStart(
dateStart,
interval ? end : monthUtils.monthFromDate(end),
interval,
firstDayOfWeekIdx,
);
}
@@ -43,6 +45,7 @@ export function validateEnd(
start: string,
end: string,
interval?: string,
firstDayOfWeekIdx?: 0 | 1 | 2 | 3 | 4 | 5 | 6,
) {
let subDays;
let dateEnd;
@@ -73,6 +76,7 @@ export function validateEnd(
interval ? start : monthUtils.monthFromDate(start),
dateEnd,
interval,
firstDayOfWeekIdx,
);
}
@@ -92,9 +96,16 @@ function boundedRange(
start: string,
end: string,
interval?: string,
firstDayOfWeekIdx?: 0 | 1 | 2 | 3 | 4 | 5 | 6,
) {
let latest;
switch (interval) {
case 'Daily':
latest = monthUtils.currentDay();
break;
case 'Weekly':
latest = monthUtils.currentWeek(firstDayOfWeekIdx);
break;
case 'Monthly':
latest = monthUtils.currentMonth() + '-31';
break;
@@ -115,13 +126,28 @@ function boundedRange(
return [start, end];
}
export function getSpecificRange(offset: number, addNumber: number | null) {
export function getSpecificRange(
offset: number,
addNumber: number | null,
type?: string,
firstDayOfWeekIdx?: 0 | 1 | 2 | 3 | 4 | 5 | 6,
) {
const currentDay = monthUtils.currentDay();
const dateStart = monthUtils.subMonths(currentDay, offset) + '-01';
const dateEnd = monthUtils.getMonthEnd(
let dateStart = monthUtils.subMonths(currentDay, offset) + '-01';
let dateEnd = monthUtils.getMonthEnd(
monthUtils.addMonths(dateStart, addNumber === null ? offset : addNumber) +
'-01',
);
if (type === 'Weeks') {
dateStart = monthUtils.subWeeks(currentDay, offset);
dateEnd = monthUtils.getWeekEnd(
monthUtils.addWeeks(dateStart, addNumber === null ? offset : addNumber),
firstDayOfWeekIdx,
);
}
return [dateStart, dateEnd];
}

View File

@@ -36,6 +36,8 @@ import { fromDateRepr } from '../util';
export function CustomReport() {
const categories = useCategories();
const [_firstDayOfWeekIdx] = useLocalPref('firstDayOfWeekIdx');
const firstDayOfWeekIdx = _firstDayOfWeekIdx || 0;
const [viewLegend = false, setViewLegendPref] =
useLocalPref('reportsViewLegend');
@@ -123,9 +125,12 @@ export function CustomReport() {
? monthUtils[format](d.parseISO(fromDateRepr(trans.date)))
: currentInterval;
const rangeProps =
interval === 'Weekly'
? [earliestInterval, currentInterval, firstDayOfWeekIdx]
: [earliestInterval, currentInterval];
const allInter = monthUtils[ReportOptions.intervalRange.get(interval)](
earliestInterval,
currentInterval,
...rangeProps,
)
.map(inter => ({
name: inter,
@@ -157,8 +162,12 @@ export function CustomReport() {
const dateStart = monthUtils[format](startDate);
const dateEnd = monthUtils[format](endDate);
const rangeProps =
interval === 'Weekly'
? [dateStart, dateEnd, firstDayOfWeekIdx]
: [dateStart, dateEnd];
setIntervals(
monthUtils[ReportOptions.intervalRange.get(interval)](dateStart, dateEnd),
monthUtils[ReportOptions.intervalRange.get(interval)](...rangeProps),
);
}, [interval, startDate, endDate]);
@@ -180,6 +189,7 @@ export function CustomReport() {
showHiddenCategories,
showUncategorized,
balanceTypeOp,
firstDayOfWeekIdx,
});
}, [
startDate,
@@ -198,6 +208,7 @@ export function CustomReport() {
showHiddenCategories,
showUncategorized,
graphType,
firstDayOfWeekIdx,
]);
const getGraphData = useMemo(() => {
@@ -219,6 +230,7 @@ export function CustomReport() {
payees,
accounts,
graphType,
firstDayOfWeekIdx,
setDataCheck,
});
}, [
@@ -238,6 +250,7 @@ export function CustomReport() {
showHiddenCategories,
showUncategorized,
graphType,
firstDayOfWeekIdx,
]);
const graphData = useReport('default', getGraphData);
const groupedData = useReport('grouped', getGroupData);
@@ -471,6 +484,7 @@ export function CustomReport() {
defaultItems={defaultItems}
defaultModeItems={defaultModeItems}
earliestTransaction={earliestTransaction}
firstDayOfWeekIdx={firstDayOfWeekIdx}
/>
<View
style={{
@@ -563,8 +577,6 @@ export function CustomReport() {
{dataCheck ? (
<ChooseGraph
startDate={startDate}
endDate={endDate}
data={data}
mode={mode}
graphType={graphType}
@@ -578,6 +590,7 @@ export function CustomReport() {
compact={false}
showHiddenCategories={showHiddenCategories}
showOffBudget={showOffBudget}
intervalsCount={intervals.length}
/>
) : (
<LoadingIndicator message="Loading report..." />

View File

@@ -101,8 +101,6 @@ export function GetCardData({
return data?.data ? (
<ErrorBoundary FallbackComponent={ErrorFallback}>
<ChooseGraph
startDate={startDate}
endDate={endDate}
data={data}
mode={report.mode}
graphType={report.graphType}

View File

@@ -47,6 +47,7 @@ export type createCustomSpreadsheetProps = {
payees?: PayeeEntity[];
accounts?: AccountEntity[];
graphType?: string;
firstDayOfWeekIdx?: 0 | 1 | 2 | 3 | 4 | 5 | 6;
setDataCheck?: (value: boolean) => void;
};
@@ -67,6 +68,7 @@ export function createCustomSpreadsheet({
payees,
accounts,
graphType,
firstDayOfWeekIdx,
setDataCheck,
}: createCustomSpreadsheetProps) {
const [categoryList, categoryGroup] = categoryLists(categories);
@@ -100,7 +102,7 @@ export function createCustomSpreadsheet({
});
const conditionsOpKey = conditionsOp === 'or' ? '$or' : '$and';
const [assets, debts] = await Promise.all([
let [assets, debts] = await Promise.all([
runQuery(
makeQuery(
'assets',
@@ -127,11 +129,33 @@ export function createCustomSpreadsheet({
).then(({ data }) => data),
]);
if (interval === 'Weekly') {
debts = debts.map(d => {
return {
...d,
date: monthUtils.weekFromDate(d.date, firstDayOfWeekIdx),
};
});
assets = assets.map(d => {
return {
...d,
date: monthUtils.weekFromDate(d.date, firstDayOfWeekIdx),
};
});
}
const format =
ReportOptions.intervalMap.get(interval).toLowerCase() + 'FromDate';
const rangeProps =
interval === 'Weekly'
? [
monthUtils[format](startDate),
monthUtils[format](endDate),
firstDayOfWeekIdx,
]
: [monthUtils[format](startDate), monthUtils[format](endDate)];
const intervals = monthUtils[ReportOptions.intervalRange.get(interval)](
monthUtils[format](startDate),
monthUtils[format](endDate),
...rangeProps,
);
let totalAssets = 0;

View File

@@ -27,6 +27,7 @@ export function createGroupedSpreadsheet({
showHiddenCategories,
showUncategorized,
balanceTypeOp,
firstDayOfWeekIdx,
}: createCustomSpreadsheetProps) {
const [categoryList, categoryGroup] = categoryLists(categories);
@@ -51,7 +52,7 @@ export function createGroupedSpreadsheet({
});
const conditionsOpKey = conditionsOp === 'or' ? '$or' : '$and';
const [assets, debts] = await Promise.all([
let [assets, debts] = await Promise.all([
runQuery(
makeQuery(
'assets',
@@ -78,11 +79,33 @@ export function createGroupedSpreadsheet({
).then(({ data }) => data),
]);
if (interval === 'Weekly') {
debts = debts.map(d => {
return {
...d,
date: monthUtils.weekFromDate(d.date, firstDayOfWeekIdx),
};
});
assets = assets.map(d => {
return {
...d,
date: monthUtils.weekFromDate(d.date, firstDayOfWeekIdx),
};
});
}
const format =
ReportOptions.intervalMap.get(interval).toLowerCase() + 'FromDate';
const rangeProps =
interval === 'Weekly'
? [
monthUtils[format](startDate),
monthUtils[format](endDate),
firstDayOfWeekIdx,
]
: [monthUtils[format](startDate), monthUtils[format](endDate)];
const intervals = monthUtils[ReportOptions.intervalRange.get(interval)](
monthUtils[format](startDate),
monthUtils[format](endDate),
...rangeProps,
);
const groupedData: DataEntity[] = categoryGroup.map(

View File

@@ -14,9 +14,15 @@ export function makeQuery(
filters: unknown[],
) {
const intervalGroup =
interval === 'Monthly' ? { $month: '$date' } : { $year: '$date' };
interval === 'Monthly'
? { $month: '$date' }
: interval === 'Yearly'
? { $year: '$date' }
: { $day: '$date' };
const intervalFilter =
'$' + ReportOptions.intervalMap.get(interval)?.toLowerCase() || 'month';
interval === 'Weekly'
? '$day'
: '$' + ReportOptions.intervalMap.get(interval)?.toLowerCase() || 'month';
const query = q('transactions')
//Apply Category_Selector

View File

@@ -600,6 +600,10 @@ const compileFunction = saveStack('function', (state, func) => {
}
// date functions
case '$day': {
validateArgLength(args, 1);
return castInput(state, args[0], 'date');
}
case '$month': {
validateArgLength(args, 1);
return castInput(state, args[0], 'date-month');

View File

@@ -87,8 +87,14 @@ 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 weekFromDate(
date: DateLike,
firstDayOfWeekIdx: 0 | 1 | 2 | 3 | 4 | 5 | 6,
): string {
return d.format(
_parse(d.startOfWeek(_parse(date), { weekStartsOn: firstDayOfWeekIdx })),
'yyyy-MM-dd',
);
}
export function dayFromDate(date: DateLike): string {
@@ -103,6 +109,19 @@ export function currentMonth(): string {
}
}
export function currentWeek(
firstDayOfWeekIdx?: 0 | 1 | 2 | 3 | 4 | 5 | 6,
): string {
if (global.IS_TESTING || Platform.isPlaywright) {
return global.currentWeek || '2017-01-01';
} else {
return d.format(
_parse(d.startOfWeek(new Date(), { weekStartsOn: firstDayOfWeekIdx })),
'yyyy-MM-dd',
);
}
}
export function currentYear(): string {
if (global.IS_TESTING || Platform.isPlaywright) {
return global.currentMonth || '2017';
@@ -169,6 +188,10 @@ export function subMonths(month: string | Date, n: number) {
return d.format(d.subMonths(_parse(month), n), 'yyyy-MM');
}
export function subWeeks(date: DateLike, n: number): string {
return d.format(d.subWeeks(_parse(date), n), 'yyyy-MM-dd');
}
export function subYears(year: string | Date, n: number) {
return d.format(d.subYears(_parse(year), n), 'yyyy');
}
@@ -221,6 +244,34 @@ export function yearRangeInclusive(start: DateLike, end: DateLike): string[] {
return _yearRange(start, end, true);
}
export function _weekRange(
start: DateLike,
end: DateLike,
inclusive = false,
firstDayOfWeekIdx?: 0 | 1 | 2 | 3 | 4 | 5 | 6,
): string[] {
const weeks: string[] = [];
let week = weekFromDate(start, firstDayOfWeekIdx);
while (d.isBefore(_parse(week), _parse(end))) {
weeks.push(week);
week = addWeeks(week, 1);
}
if (inclusive) {
weeks.push(week);
}
return weeks;
}
export function weekRangeInclusive(
start: DateLike,
end: DateLike,
firstDayOfWeekIdx?: 0 | 1 | 2 | 3 | 4 | 5 | 6,
): string[] {
return _weekRange(start, end, true, firstDayOfWeekIdx);
}
export function _range(
start: DateLike,
end: DateLike,
@@ -296,6 +347,16 @@ export function getMonthEnd(day: string): string {
return subDays(nextMonth(day.slice(0, 7)) + '-01', 1);
}
export function getWeekEnd(
date: DateLike,
firstDayOfWeekIdx?: 0 | 1 | 2 | 3 | 4 | 5 | 6,
): string {
return d.format(
_parse(d.endOfWeek(_parse(date), { weekStartsOn: firstDayOfWeekIdx })),
'yyyy-MM-dd',
);
}
export function getYearStart(month: string): string {
return getYear(month) + '-01';
}

View File

@@ -0,0 +1,6 @@
---
category: Enhancements
authors: [carkom]
---
Add daily and weekly to custom reports interval list.