Browse Source

Refactoring

* Eliminate @angular/material-moment-adapter
* Eliminate projectedTotalAmountSet
* Refactor retirementDate to type Date
pull/1748/head
Thomas 3 years ago
parent
commit
cadfa8f03e
  1. 9
      apps/api/src/app/user/update-user-setting.dto.ts
  2. 6
      apps/client/src/app/pages/portfolio/fire/fire-page.component.ts
  3. 1
      apps/client/src/app/pages/portfolio/fire/fire-page.html
  4. 11
      apps/client/src/app/services/user/user.service.ts
  5. 5
      libs/common/src/lib/interfaces/user-settings.interface.ts
  6. 9
      libs/ui/src/lib/fire-calculator/fire-calculator.component.html
  7. 133
      libs/ui/src/lib/fire-calculator/fire-calculator.component.ts
  8. 1
      package.json
  9. 1
      tsconfig.base.json

9
apps/api/src/app/user/update-user-setting.dto.ts

@ -5,6 +5,7 @@ import type {
} from '@ghostfolio/common/types'; } from '@ghostfolio/common/types';
import { import {
IsBoolean, IsBoolean,
IsDate,
IsIn, IsIn,
IsNumber, IsNumber,
IsOptional, IsOptional,
@ -48,9 +49,9 @@ export class UpdateUserSettingDto {
@IsOptional() @IsOptional()
locale?: string; locale?: string;
@IsString() @IsDate()
@IsOptional() @IsOptional()
retirementDate?: string; retirementDate?: Date;
@IsNumber() @IsNumber()
@IsOptional() @IsOptional()
@ -60,10 +61,6 @@ export class UpdateUserSettingDto {
@IsOptional() @IsOptional()
projectedTotalAmount?: number; projectedTotalAmount?: number;
@IsBoolean()
@IsOptional()
projectedTotalAmountSet?: boolean;
@IsIn(<ViewMode[]>['DEFAULT', 'ZEN']) @IsIn(<ViewMode[]>['DEFAULT', 'ZEN'])
@IsOptional() @IsOptional()
viewMode?: ViewMode; viewMode?: ViewMode;

6
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) { public onRetirementDateChange(retirementDate: Date) {
this.dataService this.dataService
.putUserSetting({ .putUserSetting({
projectedTotalAmount: null, retirementDate,
projectedTotalAmountSet: false, projectedTotalAmount: null
retirementDate: retirementDate.toISOString()
}) })
.pipe(takeUntil(this.unsubscribeSubject)) .pipe(takeUntil(this.unsubscribeSubject))
.subscribe(() => { .subscribe(() => {
@ -135,7 +134,6 @@ export class FirePageComponent implements OnDestroy, OnInit {
this.dataService this.dataService
.putUserSetting({ .putUserSetting({
projectedTotalAmount, projectedTotalAmount,
projectedTotalAmountSet: true,
retirementDate: null retirementDate: null
}) })
.pipe(takeUntil(this.unsubscribeSubject)) .pipe(takeUntil(this.unsubscribeSubject))

1
apps/client/src/app/pages/portfolio/fire/fire-page.html

@ -18,7 +18,6 @@
[hasPermissionToUpdateUserSettings]="hasPermissionToUpdateUserSettings" [hasPermissionToUpdateUserSettings]="hasPermissionToUpdateUserSettings"
[locale]="user?.settings?.locale" [locale]="user?.settings?.locale"
[projectedTotalAmount]="user?.settings?.projectedTotalAmount" [projectedTotalAmount]="user?.settings?.projectedTotalAmount"
[projectedTotalAmountSet]="user?.settings?.projectedTotalAmountSet"
[retirementDate]="user?.settings?.retirementDate" [retirementDate]="user?.settings?.retirementDate"
[savingsRate]="user?.settings?.savingsRate" [savingsRate]="user?.settings?.savingsRate"
(projectedTotalAmountChanged)="onProjectedTotalAmountChanged($event)" (projectedTotalAmountChanged)="onProjectedTotalAmountChanged($event)"

11
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 { SubscriptionInterstitialDialog } from '@ghostfolio/client/components/subscription-interstitial-dialog/subscription-interstitial-dialog.component';
import { User } from '@ghostfolio/common/interfaces'; import { User } from '@ghostfolio/common/interfaces';
import { hasPermission, permissions } from '@ghostfolio/common/permissions'; import { hasPermission, permissions } from '@ghostfolio/common/permissions';
import { parseISO } from 'date-fns';
import { DeviceDetectorService } from 'ngx-device-detector'; import { DeviceDetectorService } from 'ngx-device-detector';
import { Subject, of } from 'rxjs'; import { Subject, of, Observable } from 'rxjs';
import { throwError } from 'rxjs'; import { throwError } from 'rxjs';
import { catchError, map, takeUntil } from 'rxjs/operators'; import { catchError, map, takeUntil } from 'rxjs/operators';
@ -49,9 +50,13 @@ export class UserService extends ObservableStore<UserStoreState> {
this.setState({ user: null }, UserStoreActions.RemoveUser); this.setState({ user: null }, UserStoreActions.RemoveUser);
} }
private fetchUser() { private fetchUser(): Observable<User> {
return this.http.get<User>('/api/v1/user').pipe( return this.http.get<any>('/api/v1/user').pipe(
map((user) => { map((user) => {
if (user.settings?.retirementDate) {
user.settings.retirementDate = parseISO(user.settings.retirementDate);
}
this.setState({ user }, UserStoreActions.GetUser); this.setState({ user }, UserStoreActions.GetUser);
if ( if (

5
libs/common/src/lib/interfaces/user-settings.interface.ts

@ -10,9 +10,8 @@ export interface UserSettings {
isRestrictedView?: boolean; isRestrictedView?: boolean;
language?: string; language?: string;
locale?: string; locale?: string;
savingsRate?: number;
retirementDate?: string;
projectedTotalAmount?: number; projectedTotalAmount?: number;
projectedTotalAmountSet?: boolean; retirementDate?: Date;
savingsRate?: number;
viewMode?: ViewMode; viewMode?: ViewMode;
} }

9
libs/ui/src/lib/fire-calculator/fire-calculator.component.html

@ -33,17 +33,16 @@
<input <input
formControlName="retirementDate" formControlName="retirementDate"
matInput matInput
[matDatepicker]="dp" [matDatepicker]="datepicker"
/> />
<mat-datepicker-toggle <mat-datepicker-toggle
matIconSuffix matIconSuffix
[for]="dp" [for]="datepicker"
></mat-datepicker-toggle> ></mat-datepicker-toggle>
<mat-datepicker <mat-datepicker
#dp #datepicker
panelClass="example-month-picker"
startView="multi-year" startView="multi-year"
(monthSelected)="setMonthAndYear($event, dp)" (monthSelected)="setMonthAndYear($event, datepicker)"
> >
</mat-datepicker> </mat-datepicker>
</mat-form-field> </mat-form-field>

133
libs/ui/src/lib/fire-calculator/fire-calculator.component.ts

@ -29,62 +29,25 @@ import {
Tooltip Tooltip
} from 'chart.js'; } from 'chart.js';
import * as Color from 'color'; 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 { isNumber } from 'lodash';
import { Subject, takeUntil } from 'rxjs'; import { Subject, takeUntil } from 'rxjs';
import { FireCalculatorService } from './fire-calculator.service'; 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({ @Component({
selector: 'gf-fire-calculator',
changeDetection: ChangeDetectionStrategy.OnPush, changeDetection: ChangeDetectionStrategy.OnPush,
templateUrl: './fire-calculator.component.html', selector: 'gf-fire-calculator',
styleUrls: ['./fire-calculator.component.scss'], styleUrls: ['./fire-calculator.component.scss'],
providers: [ templateUrl: './fire-calculator.component.html'
// `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 }
]
}) })
export class FireCalculatorComponent export class FireCalculatorComponent
implements AfterViewInit, OnChanges, OnDestroy implements AfterViewInit, OnChanges, OnDestroy
@ -95,14 +58,13 @@ export class FireCalculatorComponent
@Input() fireWealth: number; @Input() fireWealth: number;
@Input() hasPermissionToUpdateUserSettings: boolean; @Input() hasPermissionToUpdateUserSettings: boolean;
@Input() locale: string; @Input() locale: string;
@Input() retirementDate: string;
@Input() savingsRate = 0;
@Input() projectedTotalAmount = 0; @Input() projectedTotalAmount = 0;
@Input() projectedTotalAmountSet: boolean; @Input() retirementDate: Date;
@Input() savingsRate = 0;
@Output() projectedTotalAmountChanged = new EventEmitter<number>();
@Output() retirementDateChanged = new EventEmitter<Date>(); @Output() retirementDateChanged = new EventEmitter<Date>();
@Output() savingsRateChanged = new EventEmitter<number>(); @Output() savingsRateChanged = new EventEmitter<number>();
@Output() projectedTotalAmountChanged = new EventEmitter<number>();
@ViewChild('chartCanvas') chartCanvas; @ViewChild('chartCanvas') chartCanvas;
@ -111,12 +73,16 @@ export class FireCalculatorComponent
paymentPerPeriod: new FormControl<number>(undefined), paymentPerPeriod: new FormControl<number>(undefined),
principalInvestmentAmount: new FormControl<number>(undefined), principalInvestmentAmount: new FormControl<number>(undefined),
projectedTotalAmount: new FormControl<number>(undefined), projectedTotalAmount: new FormControl<number>(undefined),
retirementDate: new FormControl(moment()) retirementDate: new FormControl<Date>(undefined)
}); });
public chart: Chart<'bar'>; public chart: Chart<'bar'>;
public isLoading = true; public isLoading = true;
public periodsToRetire = 0; public periodsToRetire = 0;
private readonly CONTRIBUTION_PERIOD = 12; private readonly CONTRIBUTION_PERIOD = 12;
private readonly DEFAULT_RETIREMENT_DATE = startOfMonth(
addYears(new Date(), 10)
);
private unsubscribeSubject = new Subject<void>(); private unsubscribeSubject = new Subject<void>();
public constructor( public constructor(
@ -138,7 +104,7 @@ export class FireCalculatorComponent
paymentPerPeriod: this.savingsRate, paymentPerPeriod: this.savingsRate,
principalInvestmentAmount: 0, principalInvestmentAmount: 0,
projectedTotalAmount: this.projectedTotalAmount, projectedTotalAmount: this.projectedTotalAmount,
retirementDate: moment(this.retirementDate) retirementDate: this.retirementDate ?? this.DEFAULT_RETIREMENT_DATE
}, },
{ {
emitEvent: false emitEvent: false
@ -161,7 +127,7 @@ export class FireCalculatorComponent
.get('retirementDate') .get('retirementDate')
.valueChanges.pipe(takeUntil(this.unsubscribeSubject)) .valueChanges.pipe(takeUntil(this.unsubscribeSubject))
.subscribe((retirementDate) => { .subscribe((retirementDate) => {
this.retirementDateChanged.emit(retirementDate.toDate()); this.retirementDateChanged.emit(retirementDate);
}); });
this.calculatorForm this.calculatorForm
.get('projectedTotalAmount') .get('projectedTotalAmount')
@ -179,9 +145,8 @@ export class FireCalculatorComponent
{ {
principalInvestmentAmount: this.getP(), principalInvestmentAmount: this.getP(),
paymentPerPeriod: this.getPMT(), paymentPerPeriod: this.getPMT(),
retirementDate: moment( retirementDate:
this.getRetirementDate() || DEFAULT_RETIREMENT_DATE this.getRetirementDate() ?? this.DEFAULT_RETIREMENT_DATE,
),
projectedTotalAmount: projectedTotalAmount:
Number(this.getProjectedTotalAmount().toFixed(2)) ?? 0 Number(this.getProjectedTotalAmount().toFixed(2)) ?? 0
}, },
@ -220,11 +185,10 @@ export class FireCalculatorComponent
{ {
principalInvestmentAmount: this.fireWealth, principalInvestmentAmount: this.fireWealth,
paymentPerPeriod: this.savingsRate ?? 0, paymentPerPeriod: this.savingsRate ?? 0,
retirementDate: moment(
this.getRetirementDate() || DEFAULT_RETIREMENT_DATE
),
projectedTotalAmount: projectedTotalAmount:
Number(this.getProjectedTotalAmount().toFixed(2)) ?? 0 Number(this.getProjectedTotalAmount().toFixed(2)) ?? 0,
retirementDate:
this.getRetirementDate() ?? this.DEFAULT_RETIREMENT_DATE
}, },
{ {
emitEvent: false emitEvent: false
@ -251,6 +215,19 @@ export class FireCalculatorComponent
} }
} }
public setMonthAndYear(
normalizedMonthAndYear: Date,
datepicker: MatDatepicker<Date>
) {
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() { public ngOnDestroy() {
this.chart?.destroy(); this.chart?.destroy();
@ -361,7 +338,7 @@ export class FireCalculatorComponent
} }
private getProjectedTotalAmount() { private getProjectedTotalAmount() {
if (this.projectedTotalAmountSet) { if (this.projectedTotalAmount) {
return this.projectedTotalAmount || 0; return this.projectedTotalAmount || 0;
} else { } else {
const { totalAmount } = const { totalAmount } =
@ -377,7 +354,7 @@ export class FireCalculatorComponent
} }
private getPeriodsToRetire(): number { private getPeriodsToRetire(): number {
if (this.projectedTotalAmountSet) { if (this.projectedTotalAmount) {
const periods = this.fireCalculatorService.calculatePeriodsToRetire({ const periods = this.fireCalculatorService.calculatePeriodsToRetire({
P: this.getP(), P: this.getP(),
totalAmount: this.projectedTotalAmount, totalAmount: this.projectedTotalAmount,
@ -388,9 +365,8 @@ export class FireCalculatorComponent
return periods; return periods;
} else { } else {
const today = new Date(); const today = new Date();
const retirementDate = this.retirementDate const retirementDate =
? new Date(this.retirementDate) this.retirementDate ?? this.DEFAULT_RETIREMENT_DATE;
: DEFAULT_RETIREMENT_DATE;
return ( return (
12 * (retirementDate.getFullYear() - today.getFullYear()) + 12 * (retirementDate.getFullYear() - today.getFullYear()) +
@ -401,13 +377,15 @@ export class FireCalculatorComponent
} }
private getRetirementDate(): Date { private getRetirementDate(): Date {
const yearsToRetire = Math.floor(this.periodsToRetire / 12);
const monthsToRetire = this.periodsToRetire % 12; const monthsToRetire = this.periodsToRetire % 12;
const yearsToRetire = Math.floor(this.periodsToRetire / 12);
return add(new Date(), { return startOfMonth(
years: yearsToRetire, add(new Date(), {
months: monthsToRetire months: monthsToRetire,
}); years: yearsToRetire
})
);
} }
private getChartData() { private getChartData() {
@ -485,15 +463,4 @@ export class FireCalculatorComponent
datasets: [datasetDeposit, datasetSavings, datasetInterest] datasets: [datasetDeposit, datasetSavings, datasetInterest]
}; };
} }
setMonthAndYear(
normalizedMonthAndYear: Moment,
datepicker: MatDatepicker<Moment>
) {
const ctrlValue = this.calculatorForm.get('retirementDate').value!;
ctrlValue.month(normalizedMonthAndYear.month());
ctrlValue.year(normalizedMonthAndYear.year());
this.calculatorForm.get('retirementDate').setValue(ctrlValue);
datepicker.close();
}
} }

1
package.json

@ -59,7 +59,6 @@
"@angular/core": "15.1.5", "@angular/core": "15.1.5",
"@angular/forms": "15.1.5", "@angular/forms": "15.1.5",
"@angular/material": "15.1.5", "@angular/material": "15.1.5",
"@angular/material-moment-adapter": "15.2.1",
"@angular/platform-browser": "15.1.5", "@angular/platform-browser": "15.1.5",
"@angular/platform-browser-dynamic": "15.1.5", "@angular/platform-browser-dynamic": "15.1.5",
"@angular/router": "15.1.5", "@angular/router": "15.1.5",

1
tsconfig.base.json

@ -1,7 +1,6 @@
{ {
"compileOnSave": false, "compileOnSave": false,
"compilerOptions": { "compilerOptions": {
"allowSyntheticDefaultImports": true,
"rootDir": ".", "rootDir": ".",
"sourceMap": true, "sourceMap": true,
"declaration": false, "declaration": false,

Loading…
Cancel
Save