diff --git a/apps/api/src/app/user/update-user-setting.dto.ts b/apps/api/src/app/user/update-user-setting.dto.ts index a4c844e37..63b5ae57e 100644 --- a/apps/api/src/app/user/update-user-setting.dto.ts +++ b/apps/api/src/app/user/update-user-setting.dto.ts @@ -5,6 +5,7 @@ import type { } from '@ghostfolio/common/types'; import { IsBoolean, + IsDate, IsIn, IsNumber, IsOptional, @@ -48,9 +49,9 @@ export class UpdateUserSettingDto { @IsOptional() locale?: string; - @IsString() + @IsDate() @IsOptional() - retirementDate?: string; + retirementDate?: Date; @IsNumber() @IsOptional() @@ -60,10 +61,6 @@ export class UpdateUserSettingDto { @IsOptional() projectedTotalAmount?: number; - @IsBoolean() - @IsOptional() - projectedTotalAmountSet?: boolean; - @IsIn(['DEFAULT', 'ZEN']) @IsOptional() viewMode?: ViewMode; diff --git a/apps/client/src/app/pages/portfolio/fire/fire-page.component.ts b/apps/client/src/app/pages/portfolio/fire/fire-page.component.ts index aa6439d83..9e03deb08 100644 --- a/apps/client/src/app/pages/portfolio/fire/fire-page.component.ts +++ b/apps/client/src/app/pages/portfolio/fire/fire-page.component.ts @@ -112,9 +112,8 @@ export class FirePageComponent implements OnDestroy, OnInit { public onRetirementDateChange(retirementDate: Date) { this.dataService .putUserSetting({ - projectedTotalAmount: null, - projectedTotalAmountSet: false, - retirementDate: retirementDate.toISOString() + retirementDate, + projectedTotalAmount: null }) .pipe(takeUntil(this.unsubscribeSubject)) .subscribe(() => { @@ -135,7 +134,6 @@ export class FirePageComponent implements OnDestroy, OnInit { this.dataService .putUserSetting({ projectedTotalAmount, - projectedTotalAmountSet: true, retirementDate: null }) .pipe(takeUntil(this.unsubscribeSubject)) diff --git a/apps/client/src/app/pages/portfolio/fire/fire-page.html b/apps/client/src/app/pages/portfolio/fire/fire-page.html index e9a6b02f9..c7dbbb1ed 100644 --- a/apps/client/src/app/pages/portfolio/fire/fire-page.html +++ b/apps/client/src/app/pages/portfolio/fire/fire-page.html @@ -18,7 +18,6 @@ [hasPermissionToUpdateUserSettings]="hasPermissionToUpdateUserSettings" [locale]="user?.settings?.locale" [projectedTotalAmount]="user?.settings?.projectedTotalAmount" - [projectedTotalAmountSet]="user?.settings?.projectedTotalAmountSet" [retirementDate]="user?.settings?.retirementDate" [savingsRate]="user?.settings?.savingsRate" (projectedTotalAmountChanged)="onProjectedTotalAmountChanged($event)" diff --git a/apps/client/src/app/services/user/user.service.ts b/apps/client/src/app/services/user/user.service.ts index 7f903df3a..dbfad09dd 100644 --- a/apps/client/src/app/services/user/user.service.ts +++ b/apps/client/src/app/services/user/user.service.ts @@ -6,8 +6,9 @@ import { SubscriptionInterstitialDialogParams } from '@ghostfolio/client/compone import { SubscriptionInterstitialDialog } from '@ghostfolio/client/components/subscription-interstitial-dialog/subscription-interstitial-dialog.component'; import { User } from '@ghostfolio/common/interfaces'; import { hasPermission, permissions } from '@ghostfolio/common/permissions'; +import { parseISO } from 'date-fns'; import { DeviceDetectorService } from 'ngx-device-detector'; -import { Subject, of } from 'rxjs'; +import { Subject, of, Observable } from 'rxjs'; import { throwError } from 'rxjs'; import { catchError, map, takeUntil } from 'rxjs/operators'; @@ -49,9 +50,13 @@ export class UserService extends ObservableStore { this.setState({ user: null }, UserStoreActions.RemoveUser); } - private fetchUser() { - return this.http.get('/api/v1/user').pipe( + private fetchUser(): Observable { + return this.http.get('/api/v1/user').pipe( map((user) => { + if (user.settings?.retirementDate) { + user.settings.retirementDate = parseISO(user.settings.retirementDate); + } + this.setState({ user }, UserStoreActions.GetUser); if ( diff --git a/libs/common/src/lib/interfaces/user-settings.interface.ts b/libs/common/src/lib/interfaces/user-settings.interface.ts index 44d90271d..876f3103f 100644 --- a/libs/common/src/lib/interfaces/user-settings.interface.ts +++ b/libs/common/src/lib/interfaces/user-settings.interface.ts @@ -10,9 +10,8 @@ export interface UserSettings { isRestrictedView?: boolean; language?: string; locale?: string; - savingsRate?: number; - retirementDate?: string; projectedTotalAmount?: number; - projectedTotalAmountSet?: boolean; + retirementDate?: Date; + savingsRate?: number; viewMode?: ViewMode; } diff --git a/libs/ui/src/lib/fire-calculator/fire-calculator.component.html b/libs/ui/src/lib/fire-calculator/fire-calculator.component.html index f360f212e..31451872b 100644 --- a/libs/ui/src/lib/fire-calculator/fire-calculator.component.html +++ b/libs/ui/src/lib/fire-calculator/fire-calculator.component.html @@ -33,17 +33,16 @@ diff --git a/libs/ui/src/lib/fire-calculator/fire-calculator.component.ts b/libs/ui/src/lib/fire-calculator/fire-calculator.component.ts index 3919072b8..308fba252 100644 --- a/libs/ui/src/lib/fire-calculator/fire-calculator.component.ts +++ b/libs/ui/src/lib/fire-calculator/fire-calculator.component.ts @@ -29,62 +29,25 @@ import { Tooltip } from 'chart.js'; import * as Color from 'color'; -import { add, getMonth, sub } from 'date-fns'; +import { + add, + addYears, + getMonth, + setMonth, + setYear, + startOfMonth, + sub +} from 'date-fns'; import { isNumber } from 'lodash'; import { Subject, takeUntil } from 'rxjs'; import { FireCalculatorService } from './fire-calculator.service'; -import { - DateAdapter, - MAT_DATE_FORMATS, - MAT_DATE_LOCALE -} from '@angular/material/core'; -import { - MomentDateAdapter, - MAT_MOMENT_DATE_ADAPTER_OPTIONS -} from '@angular/material-moment-adapter'; -// Depending on whether rollup is used, moment needs to be imported differently. -// Since Moment.js doesn't have a default export, we normally need to import using the `* as` -// syntax. However, rollup creates a synthetic default module and we thus need to import it using -// the `default as` syntax. -import * as _moment from 'moment'; -// tslint:disable-next-line:no-duplicate-imports -import { default as _rollupMoment, Moment } from 'moment'; - -const moment = _rollupMoment || _moment; - -// See the Moment.js docs for the meaning of these formats: -// https://momentjs.com/docs/#/displaying/format/ -export const MY_FORMATS = { - parse: { - dateInput: 'MM/YYYY' - }, - display: { - dateInput: 'MM/YYYY', - monthYearLabel: 'MMM YYYY', - dateA11yLabel: 'LL', - monthYearA11yLabel: 'MMMM YYYY' - } -}; - -export const DEFAULT_RETIREMENT_DATE = add(new Date(), { years: 10 }); @Component({ - selector: 'gf-fire-calculator', changeDetection: ChangeDetectionStrategy.OnPush, - templateUrl: './fire-calculator.component.html', + selector: 'gf-fire-calculator', styleUrls: ['./fire-calculator.component.scss'], - providers: [ - // `MomentDateAdapter` can be automatically provided by importing `MomentDateModule` in your - // application's root module. We provide it at the component level here, due to limitations of - // our example generation script. - { - provide: DateAdapter, - useClass: MomentDateAdapter, - deps: [MAT_DATE_LOCALE, MAT_MOMENT_DATE_ADAPTER_OPTIONS] - }, - { provide: MAT_DATE_FORMATS, useValue: MY_FORMATS } - ] + templateUrl: './fire-calculator.component.html' }) export class FireCalculatorComponent implements AfterViewInit, OnChanges, OnDestroy @@ -95,14 +58,13 @@ export class FireCalculatorComponent @Input() fireWealth: number; @Input() hasPermissionToUpdateUserSettings: boolean; @Input() locale: string; - @Input() retirementDate: string; - @Input() savingsRate = 0; @Input() projectedTotalAmount = 0; - @Input() projectedTotalAmountSet: boolean; + @Input() retirementDate: Date; + @Input() savingsRate = 0; + @Output() projectedTotalAmountChanged = new EventEmitter(); @Output() retirementDateChanged = new EventEmitter(); @Output() savingsRateChanged = new EventEmitter(); - @Output() projectedTotalAmountChanged = new EventEmitter(); @ViewChild('chartCanvas') chartCanvas; @@ -111,12 +73,16 @@ export class FireCalculatorComponent paymentPerPeriod: new FormControl(undefined), principalInvestmentAmount: new FormControl(undefined), projectedTotalAmount: new FormControl(undefined), - retirementDate: new FormControl(moment()) + retirementDate: new FormControl(undefined) }); public chart: Chart<'bar'>; public isLoading = true; public periodsToRetire = 0; + private readonly CONTRIBUTION_PERIOD = 12; + private readonly DEFAULT_RETIREMENT_DATE = startOfMonth( + addYears(new Date(), 10) + ); private unsubscribeSubject = new Subject(); public constructor( @@ -138,7 +104,7 @@ export class FireCalculatorComponent paymentPerPeriod: this.savingsRate, principalInvestmentAmount: 0, projectedTotalAmount: this.projectedTotalAmount, - retirementDate: moment(this.retirementDate) + retirementDate: this.retirementDate ?? this.DEFAULT_RETIREMENT_DATE }, { emitEvent: false @@ -161,7 +127,7 @@ export class FireCalculatorComponent .get('retirementDate') .valueChanges.pipe(takeUntil(this.unsubscribeSubject)) .subscribe((retirementDate) => { - this.retirementDateChanged.emit(retirementDate.toDate()); + this.retirementDateChanged.emit(retirementDate); }); this.calculatorForm .get('projectedTotalAmount') @@ -179,9 +145,8 @@ export class FireCalculatorComponent { principalInvestmentAmount: this.getP(), paymentPerPeriod: this.getPMT(), - retirementDate: moment( - this.getRetirementDate() || DEFAULT_RETIREMENT_DATE - ), + retirementDate: + this.getRetirementDate() ?? this.DEFAULT_RETIREMENT_DATE, projectedTotalAmount: Number(this.getProjectedTotalAmount().toFixed(2)) ?? 0 }, @@ -220,11 +185,10 @@ export class FireCalculatorComponent { principalInvestmentAmount: this.fireWealth, paymentPerPeriod: this.savingsRate ?? 0, - retirementDate: moment( - this.getRetirementDate() || DEFAULT_RETIREMENT_DATE - ), projectedTotalAmount: - Number(this.getProjectedTotalAmount().toFixed(2)) ?? 0 + Number(this.getProjectedTotalAmount().toFixed(2)) ?? 0, + retirementDate: + this.getRetirementDate() ?? this.DEFAULT_RETIREMENT_DATE }, { emitEvent: false @@ -251,6 +215,19 @@ export class FireCalculatorComponent } } + public setMonthAndYear( + normalizedMonthAndYear: Date, + datepicker: MatDatepicker + ) { + const ctrlValue = this.calculatorForm.get('retirementDate').value; + const newDate = setMonth( + setYear(ctrlValue, normalizedMonthAndYear.getFullYear()), + normalizedMonthAndYear.getMonth() + ); + this.calculatorForm.get('retirementDate').setValue(newDate); + datepicker.close(); + } + public ngOnDestroy() { this.chart?.destroy(); @@ -361,7 +338,7 @@ export class FireCalculatorComponent } private getProjectedTotalAmount() { - if (this.projectedTotalAmountSet) { + if (this.projectedTotalAmount) { return this.projectedTotalAmount || 0; } else { const { totalAmount } = @@ -377,7 +354,7 @@ export class FireCalculatorComponent } private getPeriodsToRetire(): number { - if (this.projectedTotalAmountSet) { + if (this.projectedTotalAmount) { const periods = this.fireCalculatorService.calculatePeriodsToRetire({ P: this.getP(), totalAmount: this.projectedTotalAmount, @@ -388,9 +365,8 @@ export class FireCalculatorComponent return periods; } else { const today = new Date(); - const retirementDate = this.retirementDate - ? new Date(this.retirementDate) - : DEFAULT_RETIREMENT_DATE; + const retirementDate = + this.retirementDate ?? this.DEFAULT_RETIREMENT_DATE; return ( 12 * (retirementDate.getFullYear() - today.getFullYear()) + @@ -401,13 +377,15 @@ export class FireCalculatorComponent } private getRetirementDate(): Date { - const yearsToRetire = Math.floor(this.periodsToRetire / 12); const monthsToRetire = this.periodsToRetire % 12; + const yearsToRetire = Math.floor(this.periodsToRetire / 12); - return add(new Date(), { - years: yearsToRetire, - months: monthsToRetire - }); + return startOfMonth( + add(new Date(), { + months: monthsToRetire, + years: yearsToRetire + }) + ); } private getChartData() { @@ -485,15 +463,4 @@ export class FireCalculatorComponent datasets: [datasetDeposit, datasetSavings, datasetInterest] }; } - - setMonthAndYear( - normalizedMonthAndYear: Moment, - datepicker: MatDatepicker - ) { - const ctrlValue = this.calculatorForm.get('retirementDate').value!; - ctrlValue.month(normalizedMonthAndYear.month()); - ctrlValue.year(normalizedMonthAndYear.year()); - this.calculatorForm.get('retirementDate').setValue(ctrlValue); - datepicker.close(); - } } diff --git a/package.json b/package.json index 3b2f138d7..d644664f7 100644 --- a/package.json +++ b/package.json @@ -59,7 +59,6 @@ "@angular/core": "15.1.5", "@angular/forms": "15.1.5", "@angular/material": "15.1.5", - "@angular/material-moment-adapter": "15.2.1", "@angular/platform-browser": "15.1.5", "@angular/platform-browser-dynamic": "15.1.5", "@angular/router": "15.1.5", diff --git a/tsconfig.base.json b/tsconfig.base.json index e250c5542..b67024e12 100644 --- a/tsconfig.base.json +++ b/tsconfig.base.json @@ -1,7 +1,6 @@ { "compileOnSave": false, "compilerOptions": { - "allowSyntheticDefaultImports": true, "rootDir": ".", "sourceMap": true, "declaration": false,