diff --git a/apps/api/src/app/activities/activities.controller.ts b/apps/api/src/app/activities/activities.controller.ts index 81af257456..589ae48a2c 100644 --- a/apps/api/src/app/activities/activities.controller.ts +++ b/apps/api/src/app/activities/activities.controller.ts @@ -83,7 +83,7 @@ export class ActivitiesController { filterByTags }); - const types = (filterByTypes?.split(',') as ActivityType[]) ?? []; + const types = this.parseActivityTypes(filterByTypes); return this.activitiesService.deleteActivities({ endDate, @@ -152,7 +152,7 @@ export class ActivitiesController { const impersonationUserId = await this.impersonationService.validateImpersonationId(impersonationId); - const types = (filterByTypes?.split(',') as ActivityType[]) ?? []; + const types = this.parseActivityTypes(filterByTypes); const userCurrency = this.request.user.settings.settings.baseCurrency; @@ -360,4 +360,28 @@ export class ActivitiesController { } }); } + + private parseActivityTypes(filterByTypes?: string): ActivityType[] { + if (!filterByTypes) { + return []; + } + + const types = filterByTypes.split(','); + + const invalidTypes = types.filter((type) => { + return !Object.values(ActivityType).includes(type as ActivityType); + }); + + if (invalidTypes.length > 0) { + throw new HttpException( + { + error: getReasonPhrase(StatusCodes.BAD_REQUEST), + message: [`Invalid activity type: ${invalidTypes.join(', ')}`] + }, + StatusCodes.BAD_REQUEST + ); + } + + return types as ActivityType[]; + } } diff --git a/apps/client/src/app/pages/portfolio/activities/activities-page.component.ts b/apps/client/src/app/pages/portfolio/activities/activities-page.component.ts index da0570142e..21704ffaf7 100644 --- a/apps/client/src/app/pages/portfolio/activities/activities-page.component.ts +++ b/apps/client/src/app/pages/portfolio/activities/activities-page.component.ts @@ -120,11 +120,11 @@ export class GfActivitiesPageComponent implements OnInit { protected onDeleteActivities() { this.dataService .deleteActivities({ - range: this.getCalendarYearRange(), activityTypes: this.activityTypesFilter.length ? this.activityTypesFilter : undefined, - filters: this.userService.getFilters() + filters: this.userService.getFilters(), + range: this.getCalendarYearRange() }) .pipe(takeUntilDestroyed(this.destroyRef)) .subscribe(() => { @@ -283,11 +283,11 @@ export class GfActivitiesPageComponent implements OnInit { this.dataService .fetchActivities({ - range: this.getCalendarYearRange(), activityTypes: this.activityTypesFilter.length ? this.activityTypesFilter : undefined, filters: this.userService.getFilters(), + range: this.getCalendarYearRange(), skip: this.pageIndex * this.pageSize, sortColumn: this.sortColumn, sortDirection: this.sortDirection, diff --git a/libs/common/src/lib/calculation-helper.ts b/libs/common/src/lib/calculation-helper.ts index 2097fa52ac..8fceea828b 100644 --- a/libs/common/src/lib/calculation-helper.ts +++ b/libs/common/src/lib/calculation-helper.ts @@ -1,3 +1,4 @@ +import { UTCDate } from '@date-fns/utc'; import { Big } from 'big.js'; import { endOfDay, @@ -7,6 +8,7 @@ import { startOfWeek, startOfYear, subDays, + subMilliseconds, subYears } from 'date-fns'; import { isFinite, isNumber } from 'lodash'; @@ -75,10 +77,20 @@ export function getIntervalFromDateRange(params: { break; case 'max': break; - default: + default: { // '2024', '2023', '2022', etc. - endDate = endOfYear(new Date(dateRange)); - startDate = max([startDate, new Date(dateRange)]); + const yearStartDate = new UTCDate(`${dateRange}-01-01`); + + // Derive the boundaries of the calendar year in UTC to be independent of + // the server's time zone, but hand out plain dates as the consumers apply + // local time zone semantics. As the start date is exclusive, the last + // millisecond of the preceding year is used. + endDate = new Date(endOfYear(yearStartDate).getTime()); + startDate = max([ + startDate, + new Date(subMilliseconds(yearStartDate, 1).getTime()) + ]); + } } return { endDate, startDate };