Browse Source

Merge 1e9d88bae1 into 2520d0d961

pull/6264/merge
Kenrick Tandrian 8 hours ago
committed by GitHub
parent
commit
3a3773e6fb
No known key found for this signature in database GPG Key ID: B5690EEEBB952194
  1. 16
      apps/client/src/app/components/benchmark-comparator/benchmark-comparator.component.ts
  2. 28
      apps/client/src/app/components/investment-chart/investment-chart.component.ts
  3. 23
      libs/common/src/lib/chart-helper.ts
  4. 2
      libs/common/src/lib/interfaces/activities.interface.ts
  5. 2
      libs/common/src/lib/interfaces/lookup-item.interface.ts
  6. 2
      libs/ui/src/lib/accounts-table/accounts-table.component.ts
  7. 26
      libs/ui/src/lib/activities-table/activities-table.component.stories.ts
  8. 2
      libs/ui/src/lib/activities-table/activities-table.component.ts
  9. 76
      libs/ui/src/lib/fire-calculator/fire-calculator.component.ts
  10. 2
      libs/ui/src/lib/holdings-table/holdings-table.component.ts
  11. 48
      libs/ui/src/lib/line-chart/line-chart.component.ts
  12. 2
      libs/ui/src/lib/mocks/holdings.ts
  13. 2
      libs/ui/src/lib/no-transactions-info/no-transactions-info.component.ts
  14. 4
      libs/ui/src/lib/notifications/alert-dialog/alert-dialog.component.ts
  15. 6
      libs/ui/src/lib/notifications/confirmation-dialog/confirmation-dialog.component.ts
  16. 10
      libs/ui/src/lib/notifications/prompt-dialog/prompt-dialog.component.ts
  17. 8
      libs/ui/src/lib/portfolio-filter-form/portfolio-filter-form.component.ts
  18. 54
      libs/ui/src/lib/portfolio-proportion-chart/portfolio-proportion-chart.component.ts
  19. 6
      libs/ui/src/lib/services/admin.service.ts
  20. 2
      libs/ui/src/lib/services/data.service.ts
  21. 2
      libs/ui/src/lib/shared/abstract-mat-form-field.ts
  22. 4
      libs/ui/src/lib/symbol-autocomplete/symbol-autocomplete.component.ts
  23. 8
      libs/ui/src/lib/tags-selector/tags-selector.component.ts
  24. 4
      libs/ui/src/lib/toggle/toggle.component.ts
  25. 13
      libs/ui/src/lib/treemap-chart/interfaces/interfaces.ts
  26. 42
      libs/ui/src/lib/treemap-chart/treemap-chart.component.ts
  27. 2
      libs/ui/src/lib/value/value.component.ts
  28. 1
      libs/ui/tsconfig.json

16
apps/client/src/app/components/benchmark-comparator/benchmark-comparator.component.ts

@ -21,6 +21,7 @@ import { CommonModule } from '@angular/common';
import { import {
ChangeDetectionStrategy, ChangeDetectionStrategy,
Component, Component,
type ElementRef,
EventEmitter, EventEmitter,
Input, Input,
OnChanges, OnChanges,
@ -42,7 +43,8 @@ import {
PointElement, PointElement,
TimeScale, TimeScale,
Tooltip, Tooltip,
TooltipPosition type TooltipOptions,
type TooltipPosition
} from 'chart.js'; } from 'chart.js';
import 'chartjs-adapter-date-fns'; import 'chartjs-adapter-date-fns';
import annotationPlugin from 'chartjs-plugin-annotation'; import annotationPlugin from 'chartjs-plugin-annotation';
@ -78,7 +80,7 @@ export class GfBenchmarkComparatorComponent implements OnChanges, OnDestroy {
@Output() benchmarkChanged = new EventEmitter<string>(); @Output() benchmarkChanged = new EventEmitter<string>();
@ViewChild('chartCanvas') chartCanvas; @ViewChild('chartCanvas') chartCanvas: ElementRef<HTMLCanvasElement>;
public chart: Chart<'line'>; public chart: Chart<'line'>;
public hasPermissionToAccessAdminControl: boolean; public hasPermissionToAccessAdminControl: boolean;
@ -158,7 +160,7 @@ export class GfBenchmarkComparatorComponent implements OnChanges, OnDestroy {
if (this.chart) { if (this.chart) {
this.chart.data = data; this.chart.data = data;
this.chart.options.plugins.tooltip = this.chart.options.plugins.tooltip =
this.getTooltipPluginConfiguration() as unknown; this.getTooltipPluginConfiguration();
this.chart.update(); this.chart.update();
} else { } else {
this.chart = new Chart(this.chartCanvas.nativeElement, { this.chart = new Chart(this.chartCanvas.nativeElement, {
@ -193,10 +195,11 @@ export class GfBenchmarkComparatorComponent implements OnChanges, OnDestroy {
display: false display: false
}, },
tooltip: this.getTooltipPluginConfiguration(), tooltip: this.getTooltipPluginConfiguration(),
// @ts-ignore
verticalHoverLine: { verticalHoverLine: {
color: `rgba(${getTextColor(this.colorScheme)}, 0.1)` color: `rgba(${getTextColor(this.colorScheme)}, 0.1)`
} }
} as unknown, },
responsive: true, responsive: true,
scales: { scales: {
x: { x: {
@ -253,7 +256,7 @@ export class GfBenchmarkComparatorComponent implements OnChanges, OnDestroy {
} }
} }
private getTooltipPluginConfiguration() { private getTooltipPluginConfiguration(): Partial<TooltipOptions<'line'>> {
return { return {
...getTooltipOptions({ ...getTooltipOptions({
colorScheme: this.colorScheme, colorScheme: this.colorScheme,
@ -261,7 +264,8 @@ export class GfBenchmarkComparatorComponent implements OnChanges, OnDestroy {
unit: '%' unit: '%'
}), }),
mode: 'index', mode: 'index',
position: 'top' as unknown, // @ts-ignore
position: 'top',
xAlign: 'center', xAlign: 'center',
yAlign: 'bottom' yAlign: 'bottom'
}; };

28
apps/client/src/app/components/investment-chart/investment-chart.component.ts

@ -20,6 +20,7 @@ import { CommonModule } from '@angular/common';
import { import {
ChangeDetectionStrategy, ChangeDetectionStrategy,
Component, Component,
type ElementRef,
Input, Input,
OnChanges, OnChanges,
OnDestroy, OnDestroy,
@ -36,7 +37,8 @@ import {
PointElement, PointElement,
TimeScale, TimeScale,
Tooltip, Tooltip,
TooltipPosition type TooltipOptions,
type TooltipPosition
} from 'chart.js'; } from 'chart.js';
import 'chartjs-adapter-date-fns'; import 'chartjs-adapter-date-fns';
import annotationPlugin from 'chartjs-plugin-annotation'; import annotationPlugin from 'chartjs-plugin-annotation';
@ -62,7 +64,7 @@ export class GfInvestmentChartComponent implements OnChanges, OnDestroy {
@Input() locale = getLocale(); @Input() locale = getLocale();
@Input() savingsRate = 0; @Input() savingsRate = 0;
@ViewChild('chartCanvas') chartCanvas; @ViewChild('chartCanvas') chartCanvas: ElementRef<HTMLCanvasElement>;
public chart: Chart<'bar' | 'line'>; public chart: Chart<'bar' | 'line'>;
private investments: InvestmentItem[]; private investments: InvestmentItem[];
@ -121,12 +123,12 @@ export class GfInvestmentChartComponent implements OnChanges, OnDestroy {
}), }),
label: this.benchmarkDataLabel, label: this.benchmarkDataLabel,
segment: { segment: {
borderColor: (context: unknown) => borderColor: (context) =>
this.isInFuture( this.isInFuture(
context, context,
`rgba(${secondaryColorRgb.r}, ${secondaryColorRgb.g}, ${secondaryColorRgb.b}, 0.67)` `rgba(${secondaryColorRgb.r}, ${secondaryColorRgb.g}, ${secondaryColorRgb.b}, 0.67)`
), ),
borderDash: (context: unknown) => this.isInFuture(context, [2, 2]) borderDash: (context) => this.isInFuture(context, [2, 2])
}, },
stepped: true stepped: true
}, },
@ -143,12 +145,12 @@ export class GfInvestmentChartComponent implements OnChanges, OnDestroy {
label: $localize`Total Amount`, label: $localize`Total Amount`,
pointRadius: 0, pointRadius: 0,
segment: { segment: {
borderColor: (context: unknown) => borderColor: (context) =>
this.isInFuture( this.isInFuture(
context, context,
`rgba(${primaryColorRgb.r}, ${primaryColorRgb.g}, ${primaryColorRgb.b}, 0.67)` `rgba(${primaryColorRgb.r}, ${primaryColorRgb.g}, ${primaryColorRgb.b}, 0.67)`
), ),
borderDash: (context: unknown) => this.isInFuture(context, [2, 2]) borderDash: (context) => this.isInFuture(context, [2, 2])
} }
} }
] ]
@ -158,7 +160,7 @@ export class GfInvestmentChartComponent implements OnChanges, OnDestroy {
if (this.chart) { if (this.chart) {
this.chart.data = chartData; this.chart.data = chartData;
this.chart.options.plugins.tooltip = this.chart.options.plugins.tooltip =
this.getTooltipPluginConfiguration() as unknown; this.getTooltipPluginConfiguration();
if ( if (
this.savingsRate && this.savingsRate &&
@ -201,7 +203,7 @@ export class GfInvestmentChartComponent implements OnChanges, OnDestroy {
color: 'white', color: 'white',
content: $localize`Savings Rate`, content: $localize`Savings Rate`,
display: true, display: true,
font: { size: '10px', weight: 'normal' }, font: { size: 10, weight: 'normal' },
padding: { padding: {
x: 4, x: 4,
y: 2 y: 2
@ -226,10 +228,11 @@ export class GfInvestmentChartComponent implements OnChanges, OnDestroy {
display: false display: false
}, },
tooltip: this.getTooltipPluginConfiguration(), tooltip: this.getTooltipPluginConfiguration(),
// @ts-ignore
verticalHoverLine: { verticalHoverLine: {
color: `rgba(${getTextColor(this.colorScheme)}, 0.1)` color: `rgba(${getTextColor(this.colorScheme)}, 0.1)`
} }
} as unknown, },
responsive: true, responsive: true,
scales: { scales: {
x: { x: {
@ -286,7 +289,9 @@ export class GfInvestmentChartComponent implements OnChanges, OnDestroy {
} }
} }
private getTooltipPluginConfiguration() { private getTooltipPluginConfiguration(): Partial<
TooltipOptions<'bar' | 'line'>
> {
return { return {
...getTooltipOptions({ ...getTooltipOptions({
colorScheme: this.colorScheme, colorScheme: this.colorScheme,
@ -296,7 +301,8 @@ export class GfInvestmentChartComponent implements OnChanges, OnDestroy {
unit: this.isInPercent ? '%' : undefined unit: this.isInPercent ? '%' : undefined
}), }),
mode: 'index', mode: 'index',
position: 'top' as unknown, // @ts-ignore
position: 'top',
xAlign: 'center', xAlign: 'center',
yAlign: 'bottom' yAlign: 'bottom'
}; };

23
libs/common/src/lib/chart-helper.ts

@ -1,10 +1,5 @@
import type { ElementRef } from '@angular/core'; import type { ElementRef } from '@angular/core';
import type { import type { Chart, Plugin, TooltipOptions, TooltipPosition } from 'chart.js';
Chart,
ChartTypeRegistry,
Plugin,
TooltipPosition
} from 'chart.js';
import { format } from 'date-fns'; import { format } from 'date-fns';
import { import {
@ -21,7 +16,7 @@ export function formatGroupedDate({
date, date,
groupBy groupBy
}: { }: {
date: Date; date: number;
groupBy: GroupBy; groupBy: GroupBy;
}) { }) {
if (groupBy === 'month') { if (groupBy === 'month') {
@ -45,27 +40,32 @@ export function getTooltipOptions({
groupBy?: GroupBy; groupBy?: GroupBy;
locale?: string; locale?: string;
unit?: string; unit?: string;
}) { }): Partial<TooltipOptions> {
return { return {
backgroundColor: getBackgroundColor(colorScheme), backgroundColor: getBackgroundColor(colorScheme),
bodyColor: `rgb(${getTextColor(colorScheme)})`, bodyColor: `rgb(${getTextColor(colorScheme)})`,
borderWidth: 1, borderWidth: 1,
borderColor: `rgba(${getTextColor(colorScheme)}, 0.1)`, borderColor: `rgba(${getTextColor(colorScheme)}, 0.1)`,
// @ts-ignore
callbacks: { callbacks: {
label: (context) => { label: (context) => {
let label = context.dataset.label ?? ''; let label = context.dataset.label ?? '';
if (label) { if (label) {
label += ': '; label += ': ';
} }
// @ts-ignore
if (context.parsed.y !== null) { if (context.parsed.y !== null) {
if (currency) { if (currency) {
// @ts-ignore
label += `${context.parsed.y.toLocaleString(locale, { label += `${context.parsed.y.toLocaleString(locale, {
maximumFractionDigits: 2, maximumFractionDigits: 2,
minimumFractionDigits: 2 minimumFractionDigits: 2
})} ${currency}`; })} ${currency}`;
} else if (unit) { } else if (unit) {
// @ts-ignore
label += `${context.parsed.y.toFixed(2)} ${unit}`; label += `${context.parsed.y.toFixed(2)} ${unit}`;
} else { } else {
// @ts-ignore
label += context.parsed.y.toFixed(2); label += context.parsed.y.toFixed(2);
} }
} }
@ -73,6 +73,7 @@ export function getTooltipOptions({
}, },
title: (contexts) => { title: (contexts) => {
if (groupBy) { if (groupBy) {
// @ts-ignore
return formatGroupedDate({ groupBy, date: contexts[0].parsed.x }); return formatGroupedDate({ groupBy, date: contexts[0].parsed.x });
} }
@ -91,8 +92,8 @@ export function getTooltipOptions({
}; };
} }
export function getTooltipPositionerMapTop( export function getTooltipPositionerMapTop<T extends 'line' | 'bar'>(
chart: Chart, chart: Chart<T>,
position: TooltipPosition position: TooltipPosition
) { ) {
if (!position || !chart?.chartArea) { if (!position || !chart?.chartArea) {
@ -104,7 +105,7 @@ export function getTooltipPositionerMapTop(
}; };
} }
export function getVerticalHoverLinePlugin<T extends keyof ChartTypeRegistry>( export function getVerticalHoverLinePlugin<T extends 'line' | 'bar'>(
chartCanvas: ElementRef, chartCanvas: ElementRef,
colorScheme: ColorScheme colorScheme: ColorScheme
): Plugin<T> { ): Plugin<T> {

2
libs/common/src/lib/interfaces/activities.interface.ts

@ -8,7 +8,7 @@ export interface Activity extends Order {
error?: ActivityError; error?: ActivityError;
feeInAssetProfileCurrency: number; feeInAssetProfileCurrency: number;
feeInBaseCurrency: number; feeInBaseCurrency: number;
SymbolProfile?: EnhancedSymbolProfile; SymbolProfile: EnhancedSymbolProfile;
tagIds?: string[]; tagIds?: string[];
tags?: Tag[]; tags?: Tag[];
unitPriceInAssetProfileCurrency: number; unitPriceInAssetProfileCurrency: number;

2
libs/common/src/lib/interfaces/lookup-item.interface.ts

@ -7,7 +7,7 @@ export interface LookupItem {
assetSubClass: AssetSubClass; assetSubClass: AssetSubClass;
currency: string; currency: string;
dataProviderInfo: DataProviderInfo; dataProviderInfo: DataProviderInfo;
dataSource: DataSource; dataSource: DataSource | null;
name: string; name: string;
symbol: string; symbol: string;
} }

2
libs/ui/src/lib/accounts-table/accounts-table.component.ts

@ -77,7 +77,7 @@ export class GfAccountsTableComponent implements OnChanges, OnDestroy {
@ViewChild(MatSort) sort: MatSort; @ViewChild(MatSort) sort: MatSort;
public dataSource = new MatTableDataSource<Account>(); public dataSource = new MatTableDataSource<Account>();
public displayedColumns = []; public displayedColumns: string[] = [];
public isLoading = true; public isLoading = true;
public routeQueryParams: Subscription; public routeQueryParams: Subscription;

26
libs/ui/src/lib/activities-table/activities-table.component.stories.ts

@ -59,7 +59,7 @@ const activities: Activity[] = [
SymbolProfile: { SymbolProfile: {
assetClass: 'EQUITY', assetClass: 'EQUITY',
assetSubClass: 'ETF', assetSubClass: 'ETF',
comment: null, comment: undefined,
countries: [], countries: [],
createdAt: new Date('2021-06-06T16:12:20.982Z'), createdAt: new Date('2021-06-06T16:12:20.982Z'),
currency: 'USD', currency: 'USD',
@ -74,12 +74,12 @@ const activities: Activity[] = [
isin: 'US9220427424', isin: 'US9220427424',
name: 'Vanguard Total World Stock Index Fund ETF Shares', name: 'Vanguard Total World Stock Index Fund ETF Shares',
updatedAt: new Date('2025-10-01T20:09:39.500Z'), updatedAt: new Date('2025-10-01T20:09:39.500Z'),
scraperConfiguration: null, scraperConfiguration: undefined,
sectors: [], sectors: [],
symbol: 'VT', symbol: 'VT',
symbolMapping: {}, symbolMapping: {},
url: 'https://www.vanguard.com', url: 'https://www.vanguard.com',
userId: null, userId: undefined,
activitiesCount: 267, activitiesCount: 267,
dateOfFirstActivity: new Date('2018-05-31T16:00:00.000Z') dateOfFirstActivity: new Date('2018-05-31T16:00:00.000Z')
}, },
@ -126,7 +126,7 @@ const activities: Activity[] = [
SymbolProfile: { SymbolProfile: {
assetClass: 'EQUITY', assetClass: 'EQUITY',
assetSubClass: 'ETF', assetSubClass: 'ETF',
comment: null, comment: undefined,
countries: [], countries: [],
createdAt: new Date('2021-06-06T16:12:20.982Z'), createdAt: new Date('2021-06-06T16:12:20.982Z'),
currency: 'USD', currency: 'USD',
@ -141,12 +141,12 @@ const activities: Activity[] = [
isin: 'US9220427424', isin: 'US9220427424',
name: 'Vanguard Total World Stock Index Fund ETF Shares', name: 'Vanguard Total World Stock Index Fund ETF Shares',
updatedAt: new Date('2025-10-01T20:09:39.500Z'), updatedAt: new Date('2025-10-01T20:09:39.500Z'),
scraperConfiguration: null, scraperConfiguration: undefined,
sectors: [], sectors: [],
symbol: 'VT', symbol: 'VT',
symbolMapping: {}, symbolMapping: {},
url: 'https://www.vanguard.com', url: 'https://www.vanguard.com',
userId: null, userId: undefined,
activitiesCount: 267, activitiesCount: 267,
dateOfFirstActivity: new Date('2018-05-31T16:00:00.000Z') dateOfFirstActivity: new Date('2018-05-31T16:00:00.000Z')
}, },
@ -193,7 +193,7 @@ const activities: Activity[] = [
SymbolProfile: { SymbolProfile: {
assetClass: 'LIQUIDITY', assetClass: 'LIQUIDITY',
assetSubClass: 'CRYPTOCURRENCY', assetSubClass: 'CRYPTOCURRENCY',
comment: null, comment: undefined,
countries: [], countries: [],
createdAt: new Date('2024-03-12T15:15:21.217Z'), createdAt: new Date('2024-03-12T15:15:21.217Z'),
currency: 'USD', currency: 'USD',
@ -208,12 +208,12 @@ const activities: Activity[] = [
isin: 'CA4639181029', isin: 'CA4639181029',
name: 'iShares Bitcoin Trust', name: 'iShares Bitcoin Trust',
updatedAt: new Date('2025-09-29T03:14:07.742Z'), updatedAt: new Date('2025-09-29T03:14:07.742Z'),
scraperConfiguration: null, scraperConfiguration: undefined,
sectors: [], sectors: [],
symbol: 'IBIT', symbol: 'IBIT',
symbolMapping: {}, symbolMapping: {},
url: 'https://www.ishares.com', url: 'https://www.ishares.com',
userId: null, userId: undefined,
activitiesCount: 6, activitiesCount: 6,
dateOfFirstActivity: new Date('2024-01-01T08:00:00.000Z') dateOfFirstActivity: new Date('2024-01-01T08:00:00.000Z')
}, },
@ -280,7 +280,7 @@ const activities: Activity[] = [
symbol: 'BNDW', symbol: 'BNDW',
symbolMapping: {}, symbolMapping: {},
url: 'https://vanguard.com', url: 'https://vanguard.com',
userId: null, userId: undefined,
activitiesCount: 38, activitiesCount: 38,
dateOfFirstActivity: new Date('2022-04-13T20:05:48.742Z') dateOfFirstActivity: new Date('2022-04-13T20:05:48.742Z')
}, },
@ -327,7 +327,7 @@ const activities: Activity[] = [
SymbolProfile: { SymbolProfile: {
assetClass: 'EQUITY', assetClass: 'EQUITY',
assetSubClass: 'ETF', assetSubClass: 'ETF',
comment: null, comment: undefined,
countries: [], countries: [],
createdAt: new Date('2021-06-06T16:12:20.982Z'), createdAt: new Date('2021-06-06T16:12:20.982Z'),
currency: 'USD', currency: 'USD',
@ -342,12 +342,12 @@ const activities: Activity[] = [
isin: 'US9220427424', isin: 'US9220427424',
name: 'Vanguard Total World Stock Index Fund ETF Shares', name: 'Vanguard Total World Stock Index Fund ETF Shares',
updatedAt: new Date('2025-10-01T20:09:39.500Z'), updatedAt: new Date('2025-10-01T20:09:39.500Z'),
scraperConfiguration: null, scraperConfiguration: undefined,
sectors: [], sectors: [],
symbol: 'VT', symbol: 'VT',
symbolMapping: {}, symbolMapping: {},
url: 'https://www.vanguard.com', url: 'https://www.vanguard.com',
userId: null, userId: undefined,
activitiesCount: 267, activitiesCount: 267,
dateOfFirstActivity: new Date('2018-05-31T16:00:00.000Z') dateOfFirstActivity: new Date('2018-05-31T16:00:00.000Z')
}, },

2
libs/ui/src/lib/activities-table/activities-table.component.ts

@ -132,7 +132,7 @@ export class GfActivitiesTableComponent
@ViewChild(MatPaginator) paginator: MatPaginator; @ViewChild(MatPaginator) paginator: MatPaginator;
@ViewChild(MatSort) sort: MatSort; @ViewChild(MatSort) sort: MatSort;
public displayedColumns = []; public displayedColumns: string[] = [];
public endOfToday = endOfToday(); public endOfToday = endOfToday();
public hasDrafts = false; public hasDrafts = false;
public hasErrors = false; public hasErrors = false;

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

@ -38,8 +38,11 @@ import {
BarElement, BarElement,
CategoryScale, CategoryScale,
Chart, Chart,
type ChartData,
type ChartDataset,
LinearScale, LinearScale,
Tooltip Tooltip,
type TooltipOptions
} from 'chart.js'; } from 'chart.js';
import 'chartjs-adapter-date-fns'; import 'chartjs-adapter-date-fns';
import Color from 'color'; import Color from 'color';
@ -98,10 +101,15 @@ export class GfFireCalculatorComponent implements OnChanges, OnDestroy {
@ViewChild('chartCanvas') chartCanvas: ElementRef<HTMLCanvasElement>; @ViewChild('chartCanvas') chartCanvas: ElementRef<HTMLCanvasElement>;
public calculatorForm = this.formBuilder.group({ public calculatorForm = this.formBuilder.group({
// @ts-ignore
annualInterestRate: new FormControl<number>(undefined), annualInterestRate: new FormControl<number>(undefined),
// @ts-ignore
paymentPerPeriod: new FormControl<number>(undefined), paymentPerPeriod: new FormControl<number>(undefined),
// @ts-ignore
principalInvestmentAmount: new FormControl<number>(undefined), principalInvestmentAmount: new FormControl<number>(undefined),
// @ts-ignore
projectedTotalAmount: new FormControl<number>(undefined), projectedTotalAmount: new FormControl<number>(undefined),
// @ts-ignore
retirementDate: new FormControl<Date>(undefined) retirementDate: new FormControl<Date>(undefined)
}); });
public chart: Chart<'bar'>; public chart: Chart<'bar'>;
@ -148,25 +156,25 @@ export class GfFireCalculatorComponent implements OnChanges, OnDestroy {
this.calculatorForm this.calculatorForm
.get('annualInterestRate') .get('annualInterestRate')
.valueChanges.pipe(debounceTime(500), takeUntil(this.unsubscribeSubject)) ?.valueChanges.pipe(debounceTime(500), takeUntil(this.unsubscribeSubject))
.subscribe((annualInterestRate) => { .subscribe((annualInterestRate) => {
this.annualInterestRateChanged.emit(annualInterestRate); this.annualInterestRateChanged.emit(annualInterestRate);
}); });
this.calculatorForm this.calculatorForm
.get('paymentPerPeriod') .get('paymentPerPeriod')
.valueChanges.pipe(debounceTime(500), takeUntil(this.unsubscribeSubject)) ?.valueChanges.pipe(debounceTime(500), takeUntil(this.unsubscribeSubject))
.subscribe((savingsRate) => { .subscribe((savingsRate) => {
this.savingsRateChanged.emit(savingsRate); this.savingsRateChanged.emit(savingsRate);
}); });
this.calculatorForm this.calculatorForm
.get('projectedTotalAmount') .get('projectedTotalAmount')
.valueChanges.pipe(debounceTime(500), takeUntil(this.unsubscribeSubject)) ?.valueChanges.pipe(debounceTime(500), takeUntil(this.unsubscribeSubject))
.subscribe((projectedTotalAmount) => { .subscribe((projectedTotalAmount) => {
this.projectedTotalAmountChanged.emit(projectedTotalAmount); this.projectedTotalAmountChanged.emit(projectedTotalAmount);
}); });
this.calculatorForm this.calculatorForm
.get('retirementDate') .get('retirementDate')
.valueChanges.pipe(debounceTime(500), takeUntil(this.unsubscribeSubject)) ?.valueChanges.pipe(debounceTime(500), takeUntil(this.unsubscribeSubject))
.subscribe((retirementDate) => { .subscribe((retirementDate) => {
this.retirementDateChanged.emit(retirementDate); this.retirementDateChanged.emit(retirementDate);
}); });
@ -194,11 +202,11 @@ export class GfFireCalculatorComponent implements OnChanges, OnDestroy {
this.calculatorForm.patchValue( this.calculatorForm.patchValue(
{ {
annualInterestRate: annualInterestRate:
this.calculatorForm.get('annualInterestRate').value, this.calculatorForm.get('annualInterestRate')?.value,
paymentPerPeriod: this.getPMT(), paymentPerPeriod: this.getPMT(),
principalInvestmentAmount: this.calculatorForm.get( principalInvestmentAmount: this.calculatorForm.get(
'principalInvestmentAmount' 'principalInvestmentAmount'
).value, )?.value,
projectedTotalAmount: projectedTotalAmount:
Math.round(this.getProjectedTotalAmount()) || 0, Math.round(this.getProjectedTotalAmount()) || 0,
retirementDate: retirementDate:
@ -208,7 +216,7 @@ export class GfFireCalculatorComponent implements OnChanges, OnDestroy {
emitEvent: false emitEvent: false
} }
); );
this.calculatorForm.get('principalInvestmentAmount').disable(); this.calculatorForm.get('principalInvestmentAmount')?.disable();
this.changeDetectorRef.markForCheck(); this.changeDetectorRef.markForCheck();
}); });
@ -217,34 +225,36 @@ export class GfFireCalculatorComponent implements OnChanges, OnDestroy {
if (this.hasPermissionToUpdateUserSettings === true) { if (this.hasPermissionToUpdateUserSettings === true) {
this.calculatorForm this.calculatorForm
.get('annualInterestRate') .get('annualInterestRate')
.enable({ emitEvent: false }); ?.enable({ emitEvent: false });
this.calculatorForm.get('paymentPerPeriod').enable({ emitEvent: false }); this.calculatorForm.get('paymentPerPeriod')?.enable({ emitEvent: false });
this.calculatorForm this.calculatorForm
.get('projectedTotalAmount') .get('projectedTotalAmount')
.enable({ emitEvent: false }); ?.enable({ emitEvent: false });
} else { } else {
this.calculatorForm this.calculatorForm
.get('annualInterestRate') .get('annualInterestRate')
.disable({ emitEvent: false }); ?.disable({ emitEvent: false });
this.calculatorForm.get('paymentPerPeriod').disable({ emitEvent: false }); this.calculatorForm
.get('paymentPerPeriod')
?.disable({ emitEvent: false });
this.calculatorForm this.calculatorForm
.get('projectedTotalAmount') .get('projectedTotalAmount')
.disable({ emitEvent: false }); ?.disable({ emitEvent: false });
} }
this.calculatorForm.get('retirementDate').disable({ emitEvent: false }); this.calculatorForm.get('retirementDate')?.disable({ emitEvent: false });
} }
public setMonthAndYear( public setMonthAndYear(
normalizedMonthAndYear: Date, normalizedMonthAndYear: Date,
datepicker: MatDatepicker<Date> datepicker: MatDatepicker<Date>
) { ) {
const retirementDate = this.calculatorForm.get('retirementDate').value; const retirementDate = this.calculatorForm.get('retirementDate')!.value;
const newRetirementDate = setMonth( const newRetirementDate = setMonth(
setYear(retirementDate, normalizedMonthAndYear.getFullYear()), setYear(retirementDate, normalizedMonthAndYear.getFullYear()),
normalizedMonthAndYear.getMonth() normalizedMonthAndYear.getMonth()
); );
this.calculatorForm.get('retirementDate').setValue(newRetirementDate); this.calculatorForm.get('retirementDate')?.setValue(newRetirementDate);
datepicker.close(); datepicker.close();
} }
@ -270,7 +280,7 @@ export class GfFireCalculatorComponent implements OnChanges, OnDestroy {
this.chart.update(); this.chart.update();
} else { } else {
this.chart = new Chart(this.chartCanvas.nativeElement, { this.chart = new Chart<'bar'>(this.chartCanvas.nativeElement, {
data: chartData, data: chartData,
options: { options: {
plugins: { plugins: {
@ -280,6 +290,7 @@ export class GfFireCalculatorComponent implements OnChanges, OnDestroy {
callbacks: { callbacks: {
footer: (items) => { footer: (items) => {
const totalAmount = items.reduce( const totalAmount = items.reduce(
// @ts-ignore
(a, b) => a + b.parsed.y, (a, b) => a + b.parsed.y,
0 0
); );
@ -312,7 +323,7 @@ export class GfFireCalculatorComponent implements OnChanges, OnDestroy {
return label; return label;
} }
} }
} } as TooltipOptions<'bar'>
}, },
responsive: true, responsive: true,
scales: { scales: {
@ -345,9 +356,9 @@ export class GfFireCalculatorComponent implements OnChanges, OnDestroy {
this.isLoading = false; this.isLoading = false;
} }
private getChartData() { private getChartData(): ChartData<'bar'> {
const currentYear = new Date().getFullYear(); const currentYear = new Date().getFullYear();
const labels = []; const labels: number[] = [];
// Principal investment amount // Principal investment amount
const P: number = this.getP(); const P: number = this.getP();
@ -360,6 +371,7 @@ export class GfFireCalculatorComponent implements OnChanges, OnDestroy {
// Calculate retirement date // Calculate retirement date
// if we want to retire at month x, we need the projectedTotalAmount at month x-1 // if we want to retire at month x, we need the projectedTotalAmount at month x-1
// @ts-ignore
const lastPeriodDate = sub(this.getRetirementDate(), { months: 1 }); const lastPeriodDate = sub(this.getRetirementDate(), { months: 1 });
const yearsToRetire = lastPeriodDate.getFullYear() - currentYear; const yearsToRetire = lastPeriodDate.getFullYear() - currentYear;
@ -371,13 +383,13 @@ export class GfFireCalculatorComponent implements OnChanges, OnDestroy {
labels.push(year); labels.push(year);
} }
const datasetDeposit = { const datasetDeposit: ChartDataset<'bar'> = {
backgroundColor: `rgb(${primaryColorRgb.r}, ${primaryColorRgb.g}, ${primaryColorRgb.b})`, backgroundColor: `rgb(${primaryColorRgb.r}, ${primaryColorRgb.g}, ${primaryColorRgb.b})`,
data: [], data: [],
label: $localize`Deposit` label: $localize`Deposit`
}; };
const datasetInterest = { const datasetInterest: ChartDataset<'bar'> = {
backgroundColor: Color( backgroundColor: Color(
`rgb(${primaryColorRgb.r}, ${primaryColorRgb.g}, ${primaryColorRgb.b})` `rgb(${primaryColorRgb.r}, ${primaryColorRgb.g}, ${primaryColorRgb.b})`
) )
@ -387,7 +399,7 @@ export class GfFireCalculatorComponent implements OnChanges, OnDestroy {
label: $localize`Interest` label: $localize`Interest`
}; };
const datasetSavings = { const datasetSavings: ChartDataset<'bar'> = {
backgroundColor: Color( backgroundColor: Color(
`rgb(${primaryColorRgb.r}, ${primaryColorRgb.g}, ${primaryColorRgb.b})` `rgb(${primaryColorRgb.r}, ${primaryColorRgb.g}, ${primaryColorRgb.b})`
) )
@ -426,12 +438,12 @@ export class GfFireCalculatorComponent implements OnChanges, OnDestroy {
} }
private getPeriodsToRetire(): number { private getPeriodsToRetire(): number {
if (this.calculatorForm.get('projectedTotalAmount').value) { if (this.calculatorForm.get('projectedTotalAmount')?.value) {
let periods = this.fireCalculatorService.calculatePeriodsToRetire({ let periods = this.fireCalculatorService.calculatePeriodsToRetire({
P: this.getP(), P: this.getP(),
PMT: this.getPMT(), PMT: this.getPMT(),
r: this.getR(), r: this.getR(),
totalAmount: this.calculatorForm.get('projectedTotalAmount').value totalAmount: this.calculatorForm.get('projectedTotalAmount')!.value
}); });
if (periods === Infinity) { if (periods === Infinity) {
@ -452,13 +464,13 @@ export class GfFireCalculatorComponent implements OnChanges, OnDestroy {
} }
} }
private getPMT() { private getPMT(): number {
return this.calculatorForm.get('paymentPerPeriod').value; return this.calculatorForm.get('paymentPerPeriod')!.value;
} }
private getProjectedTotalAmount() { private getProjectedTotalAmount() {
if (this.calculatorForm.get('projectedTotalAmount').value) { if (this.calculatorForm.get('projectedTotalAmount')?.value) {
return this.calculatorForm.get('projectedTotalAmount').value; return this.calculatorForm.get('projectedTotalAmount')!.value;
} }
const { totalAmount } = const { totalAmount } =
@ -473,10 +485,10 @@ export class GfFireCalculatorComponent implements OnChanges, OnDestroy {
} }
private getR() { private getR() {
return this.calculatorForm.get('annualInterestRate').value / 100; return this.calculatorForm.get('annualInterestRate')!.value / 100;
} }
private getRetirementDate(): Date { private getRetirementDate(): Date | undefined {
if (this.periodsToRetire === Number.MAX_SAFE_INTEGER) { if (this.periodsToRetire === Number.MAX_SAFE_INTEGER) {
return undefined; return undefined;
} }

2
libs/ui/src/lib/holdings-table/holdings-table.component.ts

@ -62,7 +62,7 @@ export class GfHoldingsTableComponent implements OnChanges, OnDestroy {
@ViewChild(MatSort) sort: MatSort; @ViewChild(MatSort) sort: MatSort;
public dataSource = new MatTableDataSource<PortfolioPosition>(); public dataSource = new MatTableDataSource<PortfolioPosition>();
public displayedColumns = []; public displayedColumns: string[] = [];
public ignoreAssetSubClasses = [AssetSubClass.CASH]; public ignoreAssetSubClasses = [AssetSubClass.CASH];
public isLoading = true; public isLoading = true;
public routeQueryParams: Subscription; public routeQueryParams: Subscription;

48
libs/ui/src/lib/line-chart/line-chart.component.ts

@ -19,12 +19,14 @@ import {
ChangeDetectionStrategy, ChangeDetectionStrategy,
ChangeDetectorRef, ChangeDetectorRef,
Component, Component,
type ElementRef,
Input, Input,
OnChanges, OnChanges,
OnDestroy, OnDestroy,
ViewChild ViewChild
} from '@angular/core'; } from '@angular/core';
import { import {
type AnimationSpec,
Chart, Chart,
Filler, Filler,
LinearScale, LinearScale,
@ -33,7 +35,8 @@ import {
PointElement, PointElement,
TimeScale, TimeScale,
Tooltip, Tooltip,
TooltipPosition type TooltipOptions,
type TooltipPosition
} from 'chart.js'; } from 'chart.js';
import 'chartjs-adapter-date-fns'; import 'chartjs-adapter-date-fns';
import { NgxSkeletonLoaderModule } from 'ngx-skeleton-loader'; import { NgxSkeletonLoaderModule } from 'ngx-skeleton-loader';
@ -67,7 +70,7 @@ export class GfLineChartComponent
@Input() yMin: number; @Input() yMin: number;
@Input() yMinLabel: string; @Input() yMinLabel: string;
@ViewChild('chartCanvas') chartCanvas; @ViewChild('chartCanvas') chartCanvas: ElementRef<HTMLCanvasElement>;
public chart: Chart<'line'>; public chart: Chart<'line'>;
public isLoading = true; public isLoading = true;
@ -117,9 +120,9 @@ export class GfLineChartComponent
private initialize() { private initialize() {
this.isLoading = true; this.isLoading = true;
const benchmarkPrices = []; const benchmarkPrices: number[] = [];
const labels: string[] = []; const labels: string[] = [];
const marketPrices = []; const marketPrices: number[] = [];
this.historicalDataItems?.forEach((historicalDataItem, index) => { this.historicalDataItems?.forEach((historicalDataItem, index) => {
benchmarkPrices.push(this.benchmarkDataItems?.[index]?.value); benchmarkPrices.push(this.benchmarkDataItems?.[index]?.value);
@ -129,10 +132,11 @@ export class GfLineChartComponent
const gradient = this.chartCanvas?.nativeElement const gradient = this.chartCanvas?.nativeElement
?.getContext('2d') ?.getContext('2d')
.createLinearGradient( ?.createLinearGradient(
0, 0,
0, 0,
0, 0,
// @ts-ignore
(this.chartCanvas.nativeElement.parentNode.offsetHeight * 4) / 5 (this.chartCanvas.nativeElement.parentNode.offsetHeight * 4) / 5
); );
@ -171,25 +175,28 @@ export class GfLineChartComponent
if (this.chartCanvas) { if (this.chartCanvas) {
if (this.chart) { if (this.chart) {
this.chart.data = data; this.chart.data = data;
if (!this.chart.options.plugins) {
this.chart.options.plugins = {};
}
this.chart.options.plugins.tooltip = this.chart.options.plugins.tooltip =
this.getTooltipPluginConfiguration() as unknown; this.getTooltipPluginConfiguration();
this.chart.options.animation = this.chart.options.animation = this.isAnimated && {
this.isAnimated && // @ts-ignore
({
x: this.getAnimationConfigurationForAxis({ labels, axis: 'x' }), x: this.getAnimationConfigurationForAxis({ labels, axis: 'x' }),
y: this.getAnimationConfigurationForAxis({ labels, axis: 'y' }) y: this.getAnimationConfigurationForAxis({ labels, axis: 'y' })
} as unknown); };
this.chart.update(); this.chart.update();
} else { } else {
this.chart = new Chart(this.chartCanvas.nativeElement, { this.chart = new Chart(this.chartCanvas.nativeElement, {
data, data,
options: { options: {
animation: animation: this.isAnimated && {
this.isAnimated && // @ts-ignore
({
x: this.getAnimationConfigurationForAxis({ labels, axis: 'x' }), x: this.getAnimationConfigurationForAxis({ labels, axis: 'x' }),
y: this.getAnimationConfigurationForAxis({ labels, axis: 'y' }) y: this.getAnimationConfigurationForAxis({ labels, axis: 'y' })
} as unknown), },
aspectRatio: 16 / 9, aspectRatio: 16 / 9,
elements: { elements: {
point: { point: {
@ -205,10 +212,11 @@ export class GfLineChartComponent
position: 'bottom' position: 'bottom'
}, },
tooltip: this.getTooltipPluginConfiguration(), tooltip: this.getTooltipPluginConfiguration(),
// @ts-ignore
verticalHoverLine: { verticalHoverLine: {
color: `rgba(${getTextColor(this.colorScheme)}, 0.1)` color: `rgba(${getTextColor(this.colorScheme)}, 0.1)`
} }
} as unknown, },
scales: { scales: {
x: { x: {
border: { border: {
@ -298,7 +306,7 @@ export class GfLineChartComponent
}: { }: {
axis: 'x' | 'y'; axis: 'x' | 'y';
labels: string[]; labels: string[];
}) { }): AnimationSpec<'line'> {
const delayBetweenPoints = this.ANIMATION_DURATION / labels.length; const delayBetweenPoints = this.ANIMATION_DURATION / labels.length;
return { return {
@ -308,16 +316,19 @@ export class GfLineChartComponent
} }
context[`${axis}Started`] = true; context[`${axis}Started`] = true;
// @ts-ignore
return context.index * delayBetweenPoints; return context.index * delayBetweenPoints;
}, },
duration: delayBetweenPoints, duration: delayBetweenPoints,
easing: 'linear', easing: 'linear',
// @ts-ignore
from: NaN, from: NaN,
// @ts-ignore
type: 'number' type: 'number'
}; };
} }
private getTooltipPluginConfiguration() { private getTooltipPluginConfiguration(): Partial<TooltipOptions<'line'>> {
return { return {
...getTooltipOptions({ ...getTooltipOptions({
colorScheme: this.colorScheme, colorScheme: this.colorScheme,
@ -326,7 +337,8 @@ export class GfLineChartComponent
unit: this.unit unit: this.unit
}), }),
mode: 'index', mode: 'index',
position: 'top' as unknown, // @ts-ignore
position: 'top',
xAlign: 'center', xAlign: 'center',
yAlign: 'bottom' yAlign: 'bottom'
}; };

2
libs/ui/src/lib/mocks/holdings.ts

@ -162,7 +162,7 @@ export const holdings: PortfolioPosition[] = [
symbol: 'bitcoin', symbol: 'bitcoin',
tags: [], tags: [],
transactionCount: 1, transactionCount: 1,
url: null, url: undefined,
valueInBaseCurrency: 54666.7898248 valueInBaseCurrency: 54666.7898248
}, },
{ {

2
libs/ui/src/lib/no-transactions-info/no-transactions-info.component.ts

@ -24,5 +24,5 @@ export class GfNoTransactionsInfoComponent {
@HostBinding('class.has-border') @Input() hasBorder = true; @HostBinding('class.has-border') @Input() hasBorder = true;
public routerLinkPortfolioActivities = public routerLinkPortfolioActivities =
internalRoutes.portfolio.subRoutes.activities.routerLink; internalRoutes.portfolio.subRoutes?.activities.routerLink;
} }

4
libs/ui/src/lib/notifications/alert-dialog/alert-dialog.component.ts

@ -11,8 +11,8 @@ import { AlertDialogParams } from './interfaces/interfaces';
templateUrl: './alert-dialog.html' templateUrl: './alert-dialog.html'
}) })
export class GfAlertDialogComponent { export class GfAlertDialogComponent {
public discardLabel: string; public discardLabel?: string;
public message: string; public message?: string;
public title: string; public title: string;
public constructor(public dialogRef: MatDialogRef<GfAlertDialogComponent>) {} public constructor(public dialogRef: MatDialogRef<GfAlertDialogComponent>) {}

6
libs/ui/src/lib/notifications/confirmation-dialog/confirmation-dialog.component.ts

@ -13,10 +13,10 @@ import { ConfirmDialogParams } from './interfaces/interfaces';
templateUrl: './confirmation-dialog.html' templateUrl: './confirmation-dialog.html'
}) })
export class GfConfirmationDialogComponent { export class GfConfirmationDialogComponent {
public confirmLabel: string; public confirmLabel?: string;
public confirmType: ConfirmationDialogType; public confirmType: ConfirmationDialogType;
public discardLabel: string; public discardLabel?: string;
public message: string; public message?: string;
public title: string; public title: string;
public constructor( public constructor(

10
libs/ui/src/lib/notifications/prompt-dialog/prompt-dialog.component.ts

@ -18,25 +18,25 @@ import { MatInputModule } from '@angular/material/input';
}) })
export class GfPromptDialogComponent { export class GfPromptDialogComponent {
public confirmLabel: string; public confirmLabel: string;
public defaultValue: string; public defaultValue?: string;
public discardLabel: string; public discardLabel: string;
public formControl = new FormControl(''); public formControl = new FormControl('');
public title: string; public title: string;
public valueLabel: string; public valueLabel?: string;
public constructor(public dialogRef: MatDialogRef<GfPromptDialogComponent>) {} public constructor(public dialogRef: MatDialogRef<GfPromptDialogComponent>) {}
public initialize(aParams: { public initialize(aParams: {
confirmLabel?: string; confirmLabel: string;
defaultValue?: string; defaultValue?: string;
discardLabel?: string; discardLabel: string;
title: string; title: string;
valueLabel?: string; valueLabel?: string;
}) { }) {
this.confirmLabel = aParams.confirmLabel; this.confirmLabel = aParams.confirmLabel;
this.defaultValue = aParams.defaultValue; this.defaultValue = aParams.defaultValue;
this.discardLabel = aParams.discardLabel; this.discardLabel = aParams.discardLabel;
this.formControl.setValue(aParams.defaultValue); this.formControl.setValue(aParams.defaultValue ?? null);
this.title = aParams.title; this.title = aParams.title;
this.valueLabel = aParams.valueLabel; this.valueLabel = aParams.valueLabel;
} }

8
libs/ui/src/lib/portfolio-filter-form/portfolio-filter-form.component.ts

@ -70,10 +70,10 @@ export class GfPortfolioFilterFormComponent
private formBuilder: FormBuilder private formBuilder: FormBuilder
) { ) {
this.filterForm = this.formBuilder.group({ this.filterForm = this.formBuilder.group({
account: new FormControl<string>(null), account: new FormControl<string | null>(null),
assetClass: new FormControl<string>(null), assetClass: new FormControl<string | null>(null),
holding: new FormControl<PortfolioPosition>(null), holding: new FormControl<PortfolioPosition | null>(null),
tag: new FormControl<string>(null) tag: new FormControl<string | null>(null)
}); });
} }

54
libs/ui/src/lib/portfolio-proportion-chart/portfolio-proportion-chart.component.ts

@ -22,11 +22,18 @@ import {
} from '@angular/core'; } from '@angular/core';
import { DataSource } from '@prisma/client'; import { DataSource } from '@prisma/client';
import { Big } from 'big.js'; import { Big } from 'big.js';
import { ChartConfiguration, Tooltip } from 'chart.js'; import {
import { LinearScale } from 'chart.js'; ArcElement,
import { ArcElement } from 'chart.js'; Chart,
import { DoughnutController } from 'chart.js'; type ChartData,
import { Chart } from 'chart.js'; type ChartDataset,
DoughnutController,
LinearScale,
type Plugin,
Tooltip,
type TooltipItem,
type TooltipOptions
} from 'chart.js';
import ChartDataLabels from 'chartjs-plugin-datalabels'; import ChartDataLabels from 'chartjs-plugin-datalabels';
import { isUUID } from 'class-validator'; import { isUUID } from 'class-validator';
import Color from 'color'; import Color from 'color';
@ -135,16 +142,18 @@ export class GfPortfolioProportionChartComponent
if ( if (
chartData[this.data[symbol][this.keys[0]].toUpperCase()] chartData[this.data[symbol][this.keys[0]].toUpperCase()]
.subCategory[this.data[symbol][this.keys[1]]] ?.subCategory?.[this.data[symbol][this.keys[1]]]
) { ) {
// @ts-ignore
chartData[ chartData[
this.data[symbol][this.keys[0]].toUpperCase() this.data[symbol][this.keys[0]].toUpperCase()
].subCategory[this.data[symbol][this.keys[1]]].value = chartData[ ].subCategory[this.data[symbol][this.keys[1]]].value = chartData[
this.data[symbol][this.keys[0]].toUpperCase() this.data[symbol][this.keys[0]].toUpperCase()
].subCategory[this.data[symbol][this.keys[1]]].value.plus( ].subCategory?.[this.data[symbol][this.keys[1]]].value.plus(
this.data[symbol].value || 0 this.data[symbol].value || 0
); );
} else { } else {
// @ts-ignore
chartData[ chartData[
this.data[symbol][this.keys[0]].toUpperCase() this.data[symbol][this.keys[0]].toUpperCase()
].subCategory[this.data[symbol][this.keys[1]] ?? UNKNOWN_KEY] = { ].subCategory[this.data[symbol][this.keys[1]] ?? UNKNOWN_KEY] = {
@ -273,12 +282,14 @@ export class GfPortfolioProportionChartComponent
Object.keys(item.subCategory ?? {}).forEach((subCategory) => { Object.keys(item.subCategory ?? {}).forEach((subCategory) => {
if (item.name === UNKNOWN_KEY) { if (item.name === UNKNOWN_KEY) {
// @ts-ignore
backgroundColorSubCategory.push(item.color); backgroundColorSubCategory.push(item.color);
} else { } else {
backgroundColorSubCategory.push( backgroundColorSubCategory.push(
Color(item.color).lighten(lightnessRatio).hex() Color(item.color).lighten(lightnessRatio).hex()
); );
} }
// @ts-ignore
dataSubCategory.push(item.subCategory[subCategory].value.toNumber()); dataSubCategory.push(item.subCategory[subCategory].value.toNumber());
labelSubCategory.push(subCategory); labelSubCategory.push(subCategory);
@ -286,7 +297,7 @@ export class GfPortfolioProportionChartComponent
}); });
}); });
const datasets: ChartConfiguration<'doughnut'>['data']['datasets'] = [ const datasets: ChartDataset<'doughnut'>[] = [
{ {
backgroundColor: chartDataSorted.map(([, item]) => { backgroundColor: chartDataSorted.map(([, item]) => {
return item.color; return item.color;
@ -324,7 +335,7 @@ export class GfPortfolioProportionChartComponent
datasets[1].data[1] = Number.MAX_SAFE_INTEGER; datasets[1].data[1] = Number.MAX_SAFE_INTEGER;
} }
const data: ChartConfiguration<'doughnut'>['data'] = { const data: ChartData<'doughnut'> = {
datasets, datasets,
labels labels
}; };
@ -332,9 +343,13 @@ export class GfPortfolioProportionChartComponent
if (this.chartCanvas) { if (this.chartCanvas) {
if (this.chart) { if (this.chart) {
this.chart.data = data; this.chart.data = data;
this.chart.options.plugins.tooltip = this.getTooltipPluginConfiguration(
data if (!this.chart.options.plugins) {
) as unknown; this.chart.options.plugins = {};
}
this.chart.options.plugins.tooltip =
this.getTooltipPluginConfiguration(data);
this.chart.update(); this.chart.update();
} else { } else {
this.chart = new Chart<'doughnut'>(this.chartCanvas.nativeElement, { this.chart = new Chart<'doughnut'>(this.chartCanvas.nativeElement, {
@ -348,15 +363,17 @@ export class GfPortfolioProportionChartComponent
onClick: (event, activeElements) => { onClick: (event, activeElements) => {
try { try {
const dataIndex = activeElements[0].index; const dataIndex = activeElements[0].index;
// @ts-ignore
const symbol: string = event.chart.data.labels[dataIndex]; const symbol: string = event.chart.data.labels[dataIndex];
const dataSource = this.data[symbol]?.dataSource; const dataSource = this.data[symbol].dataSource!;
this.proportionChartClicked.emit({ dataSource, symbol }); this.proportionChartClicked.emit({ dataSource, symbol });
} catch {} } catch {}
}, },
onHover: (event, chartElement) => { onHover: (event, chartElement) => {
if (this.cursor) { if (this.cursor) {
// @ts-ignore
event.native.target.style.cursor = chartElement[0] event.native.target.style.cursor = chartElement[0]
? this.cursor ? this.cursor
: 'default'; : 'default';
@ -392,8 +409,8 @@ export class GfPortfolioProportionChartComponent
legend: { display: false }, legend: { display: false },
tooltip: this.getTooltipPluginConfiguration(data) tooltip: this.getTooltipPluginConfiguration(data)
} }
} as unknown, },
plugins: [ChartDataLabels], plugins: [ChartDataLabels as Plugin<'doughnut'>],
type: 'doughnut' type: 'doughnut'
}); });
} }
@ -419,15 +436,18 @@ export class GfPortfolioProportionChartComponent
]; ];
} }
private getTooltipPluginConfiguration(data: ChartConfiguration['data']) { private getTooltipPluginConfiguration(
data: ChartData<'doughnut'>
): Partial<TooltipOptions<'doughnut'>> {
return { return {
...getTooltipOptions({ ...getTooltipOptions({
colorScheme: this.colorScheme, colorScheme: this.colorScheme,
currency: this.baseCurrency, currency: this.baseCurrency,
locale: this.locale locale: this.locale
}), }),
// @ts-ignore
callbacks: { callbacks: {
label: (context) => { label: (context: TooltipItem<'doughnut'>) => {
const labelIndex = const labelIndex =
(data.datasets[context.datasetIndex - 1]?.data?.length ?? 0) + (data.datasets[context.datasetIndex - 1]?.data?.length ?? 0) +
context.dataIndex; context.dataIndex;

6
libs/ui/src/lib/services/admin.service.ts

@ -132,7 +132,7 @@ export class AdminService {
public fetchJobs({ status }: { status?: JobStatus[] }) { public fetchJobs({ status }: { status?: JobStatus[] }) {
let params = new HttpParams(); let params = new HttpParams();
if (status?.length > 0) { if (status && status.length > 0) {
params = params.append('status', status.join(',')); params = params.append('status', status.join(','));
} }
@ -158,8 +158,8 @@ export class AdminService {
}) { }) {
let params = new HttpParams(); let params = new HttpParams();
params = params.append('skip', skip); if (skip) params = params.append('skip', skip);
params = params.append('take', take); if (take) params = params.append('take', take);
return this.http.get<AdminUsersResponse>('/api/v1/admin/user', { params }); return this.http.get<AdminUsersResponse>('/api/v1/admin/user', { params });
} }

2
libs/ui/src/lib/services/data.service.ts

@ -89,7 +89,7 @@ export class DataService {
public buildFiltersAsQueryParams({ filters }: { filters?: Filter[] }) { public buildFiltersAsQueryParams({ filters }: { filters?: Filter[] }) {
let params = new HttpParams(); let params = new HttpParams();
if (filters?.length > 0) { if (filters && filters.length > 0) {
const { const {
ACCOUNT: filtersByAccount, ACCOUNT: filtersByAccount,
ASSET_CLASS: filtersByAssetClass, ASSET_CLASS: filtersByAssetClass,

2
libs/ui/src/lib/shared/abstract-mat-form-field.ts

@ -137,7 +137,7 @@ export abstract class AbstractMatFormField<T>
public ngDoCheck() { public ngDoCheck() {
if (this.ngControl) { if (this.ngControl) {
this.errorState = this.ngControl.invalid && this.ngControl.touched; this.errorState = !!(this.ngControl.invalid && this.ngControl.touched);
this.stateChanges.next(); this.stateChanges.next();
} }
} }

4
libs/ui/src/lib/symbol-autocomplete/symbol-autocomplete.component.ts

@ -185,7 +185,7 @@ export class GfSymbolAutocompleteComponent
public ngDoCheck() { public ngDoCheck() {
if (this.ngControl) { if (this.ngControl) {
this.validateRequired(); this.validateRequired();
this.errorState = this.ngControl.invalid && this.ngControl.touched; this.errorState = !!(this.ngControl.invalid && this.ngControl.touched);
this.stateChanges.next(); this.stateChanges.next();
} }
} }
@ -225,7 +225,7 @@ export class GfSymbolAutocompleteComponent
? !super.value?.dataSource || !super.value?.symbol ? !super.value?.dataSource || !super.value?.symbol
: false; : false;
if (requiredCheck) { if (requiredCheck) {
this.ngControl.control.setErrors({ invalidData: true }); this.ngControl.control?.setErrors({ invalidData: true });
} }
} }
} }

8
libs/ui/src/lib/tags-selector/tags-selector.component.ts

@ -77,7 +77,7 @@ export class GfTagsSelectorComponent
this.tagInputControl.valueChanges this.tagInputControl.valueChanges
.pipe(takeUntil(this.unsubscribeSubject)) .pipe(takeUntil(this.unsubscribeSubject))
.subscribe((value) => { .subscribe((value) => {
this.filteredOptions.next(this.filterTags(value)); this.filteredOptions.next(this.filterTags(value ?? ''));
}); });
addIcons({ addCircleOutline, closeOutline }); addIcons({ addCircleOutline, closeOutline });
@ -100,21 +100,23 @@ export class GfTagsSelectorComponent
if (!tag && this.hasPermissionToCreateTag) { if (!tag && this.hasPermissionToCreateTag) {
tag = { tag = {
id: undefined, id: '',
name: event.option.value as string, name: event.option.value as string,
userId: null userId: null
}; };
} }
if (tag) {
this.tagsSelected.update((tags) => { this.tagsSelected.update((tags) => {
return [...(tags ?? []), tag]; return [...(tags ?? []), tag];
}); });
}
const newTags = this.tagsSelected(); const newTags = this.tagsSelected();
this.onChange(newTags); this.onChange(newTags);
this.onTouched(); this.onTouched();
this.tagInput.nativeElement.value = ''; this.tagInput.nativeElement.value = '';
this.tagInputControl.setValue(undefined); this.tagInputControl.setValue(null);
} }
public onRemoveTag(tag: Tag) { public onRemoveTag(tag: Tag) {

4
libs/ui/src/lib/toggle/toggle.component.ts

@ -26,13 +26,15 @@ export class GfToggleComponent implements OnChanges {
@Output() valueChange = new EventEmitter<Pick<ToggleOption, 'value'>>(); @Output() valueChange = new EventEmitter<Pick<ToggleOption, 'value'>>();
public optionFormControl = new FormControl<string>(undefined); public optionFormControl = new FormControl<string | null>(null);
public ngOnChanges() { public ngOnChanges() {
this.optionFormControl.setValue(this.defaultValue); this.optionFormControl.setValue(this.defaultValue);
} }
public onValueChange() { public onValueChange() {
if (this.optionFormControl.value !== null) {
this.valueChange.emit({ value: this.optionFormControl.value }); this.valueChange.emit({ value: this.optionFormControl.value });
} }
} }
}

13
libs/ui/src/lib/treemap-chart/interfaces/interfaces.ts

@ -1,5 +1,18 @@
import { PortfolioPosition } from '@ghostfolio/common/interfaces';
import {
TreemapDataPoint,
TreemapScriptableContext
} from 'chartjs-chart-treemap';
export interface GetColorParams { export interface GetColorParams {
annualizedNetPerformancePercent: number; annualizedNetPerformancePercent: number;
negativeNetPerformancePercentsRange: { max: number; min: number }; negativeNetPerformancePercentsRange: { max: number; min: number };
positiveNetPerformancePercentsRange: { max: number; min: number }; positiveNetPerformancePercentsRange: { max: number; min: number };
} }
export interface GfTreemapChartTooltipContext extends TreemapScriptableContext {
raw: TreemapDataPoint & {
_data: PortfolioPosition;
};
}

42
libs/ui/src/lib/treemap-chart/treemap-chart.component.ts

@ -25,7 +25,7 @@ import {
} from '@angular/core'; } from '@angular/core';
import { DataSource } from '@prisma/client'; import { DataSource } from '@prisma/client';
import { Big } from 'big.js'; import { Big } from 'big.js';
import { ChartConfiguration } from 'chart.js'; import type { ChartData, TooltipOptions } from 'chart.js';
import { LinearScale } from 'chart.js'; import { LinearScale } from 'chart.js';
import { Chart, Tooltip } from 'chart.js'; import { Chart, Tooltip } from 'chart.js';
import { TreemapController, TreemapElement } from 'chartjs-chart-treemap'; import { TreemapController, TreemapElement } from 'chartjs-chart-treemap';
@ -35,7 +35,10 @@ import { orderBy } from 'lodash';
import { NgxSkeletonLoaderModule } from 'ngx-skeleton-loader'; import { NgxSkeletonLoaderModule } from 'ngx-skeleton-loader';
import OpenColor from 'open-color'; import OpenColor from 'open-color';
import { GetColorParams } from './interfaces/interfaces'; import {
GetColorParams,
GfTreemapChartTooltipContext
} from './interfaces/interfaces';
const { gray, green, red } = OpenColor; const { gray, green, red } = OpenColor;
@ -198,10 +201,10 @@ export class GfTreemapChartComponent
min: Math.min(...negativeNetPerformancePercents) min: Math.min(...negativeNetPerformancePercents)
}; };
const data: ChartConfiguration<'treemap'>['data'] = { const data: ChartData<'treemap'> = {
datasets: [ datasets: [
{ {
backgroundColor: (context) => { backgroundColor: (context: GfTreemapChartTooltipContext) => {
let annualizedNetPerformancePercent = let annualizedNetPerformancePercent =
getAnnualizedPerformancePercent({ getAnnualizedPerformancePercent({
daysInMarket: differenceInDays( daysInMarket: differenceInDays(
@ -232,7 +235,7 @@ export class GfTreemapChartComponent
key: 'allocationInPercentage', key: 'allocationInPercentage',
labels: { labels: {
align: 'left', align: 'left',
color: (context) => { color: (context: GfTreemapChartTooltipContext) => {
let annualizedNetPerformancePercent = let annualizedNetPerformancePercent =
getAnnualizedPerformancePercent({ getAnnualizedPerformancePercent({
daysInMarket: differenceInDays( daysInMarket: differenceInDays(
@ -261,7 +264,7 @@ export class GfTreemapChartComponent
}, },
display: true, display: true,
font: [{ size: 16 }, { lineHeight: 1.5, size: 14 }], font: [{ size: 16 }, { lineHeight: 1.5, size: 14 }],
formatter: ({ raw }) => { formatter: ({ raw }: GfTreemapChartTooltipContext) => {
// Round to 4 decimal places // Round to 4 decimal places
let netPerformancePercentWithCurrencyEffect = let netPerformancePercentWithCurrencyEffect =
Math.round( Math.round(
@ -286,19 +289,25 @@ export class GfTreemapChartComponent
position: 'top' position: 'top'
}, },
spacing: 1, spacing: 1,
// @ts-ignore
tree: this.holdings tree: this.holdings
} }
] ]
} as any; };
if (this.chartCanvas) { if (this.chartCanvas) {
if (this.chart) { if (this.chart) {
this.chart.data = data; this.chart.data = data;
if (!this.chart.options.plugins) {
this.chart.options.plugins = {};
}
this.chart.options.plugins.tooltip = this.chart.options.plugins.tooltip =
this.getTooltipPluginConfiguration() as unknown; this.getTooltipPluginConfiguration();
this.chart.update(); this.chart.update();
} else { } else {
this.chart = new Chart(this.chartCanvas.nativeElement, { this.chart = new Chart<'treemap'>(this.chartCanvas.nativeElement, {
data, data,
options: { options: {
animation: false, animation: false,
@ -308,6 +317,7 @@ export class GfTreemapChartComponent
const datasetIndex = activeElements[0].datasetIndex; const datasetIndex = activeElements[0].datasetIndex;
const dataset = orderBy( const dataset = orderBy(
// @ts-ignore
event.chart.data.datasets[datasetIndex].tree, event.chart.data.datasets[datasetIndex].tree,
['allocationInPercentage'], ['allocationInPercentage'],
['desc'] ['desc']
@ -321,6 +331,7 @@ export class GfTreemapChartComponent
}, },
onHover: (event, chartElement) => { onHover: (event, chartElement) => {
if (this.cursor) { if (this.cursor) {
// @ts-ignore
event.native.target.style.cursor = chartElement[0] event.native.target.style.cursor = chartElement[0]
? this.cursor ? this.cursor
: 'default'; : 'default';
@ -329,7 +340,7 @@ export class GfTreemapChartComponent
plugins: { plugins: {
tooltip: this.getTooltipPluginConfiguration() tooltip: this.getTooltipPluginConfiguration()
} }
} as unknown, },
type: 'treemap' type: 'treemap'
}); });
} }
@ -338,7 +349,7 @@ export class GfTreemapChartComponent
this.isLoading = false; this.isLoading = false;
} }
private getTooltipPluginConfiguration() { private getTooltipPluginConfiguration(): Partial<TooltipOptions<'treemap'>> {
return { return {
...getTooltipOptions({ ...getTooltipOptions({
colorScheme: this.colorScheme, colorScheme: this.colorScheme,
@ -346,8 +357,9 @@ export class GfTreemapChartComponent
locale: this.locale locale: this.locale
}), }),
callbacks: { callbacks: {
label: ({ raw }) => { // @ts-ignore
const allocationInPercentage = `${((raw._data.allocationInPercentage as number) * 100).toFixed(2)}%`; label: ({ raw }: GfTreemapChartTooltipContext) => {
const allocationInPercentage = `${(raw._data.allocationInPercentage * 100).toFixed(2)}%`;
const name = raw._data.name; const name = raw._data.name;
const sign = const sign =
raw._data.netPerformancePercentWithCurrencyEffect > 0 ? '+' : ''; raw._data.netPerformancePercentWithCurrencyEffect > 0 ? '+' : '';
@ -356,11 +368,11 @@ export class GfTreemapChartComponent
const netPerformanceInPercentageWithSign = `${sign}${(raw._data.netPerformancePercentWithCurrencyEffect * 100).toFixed(2)}%`; const netPerformanceInPercentageWithSign = `${sign}${(raw._data.netPerformancePercentWithCurrencyEffect * 100).toFixed(2)}%`;
if (raw._data.valueInBaseCurrency !== null) { if (raw._data.valueInBaseCurrency !== null) {
const value = raw._data.valueInBaseCurrency as number; const value = raw._data.valueInBaseCurrency;
return [ return [
`${name ?? symbol} (${allocationInPercentage})`, `${name ?? symbol} (${allocationInPercentage})`,
`${value.toLocaleString(this.locale, { `${value?.toLocaleString(this.locale, {
maximumFractionDigits: 2, maximumFractionDigits: 2,
minimumFractionDigits: 2 minimumFractionDigits: 2
})} ${this.baseCurrency}`, })} ${this.baseCurrency}`,

2
libs/ui/src/lib/value/value.component.ts

@ -139,7 +139,7 @@ export class GfValueComponent implements OnChanges {
this.isNumber = false; this.isNumber = false;
this.isString = false; this.isString = false;
this.locale = this.locale || getLocale(); this.locale = this.locale || getLocale();
this.precision = this.precision >= 0 ? this.precision : undefined; this.precision = this.precision >= 0 ? this.precision : 0;
this.useAbsoluteValue = false; this.useAbsoluteValue = false;
} }
} }

1
libs/ui/tsconfig.json

@ -19,6 +19,7 @@
"target": "es2020", "target": "es2020",
// TODO: Remove once solved in tsconfig.base.json // TODO: Remove once solved in tsconfig.base.json
"strict": false, "strict": false,
"strictNullChecks": true,
"noImplicitReturns": true, "noImplicitReturns": true,
"noFallthroughCasesInSwitch": true "noFallthroughCasesInSwitch": true
}, },

Loading…
Cancel
Save