Browse Source

Merge branch 'main' into feature/improve-validation-for-currency-in-endpoints

pull/3030/head
Thomas Kaul 2 years ago
committed by GitHub
parent
commit
339a3a4100
No known key found for this signature in database GPG Key ID: B5690EEEBB952194
  1. 1
      CHANGELOG.md
  2. 14
      apps/client/src/app/components/account-detail-dialog/account-detail-dialog.html
  3. 4
      apps/client/src/app/components/accounts-table/accounts-table.component.ts
  4. 25
      apps/client/src/app/components/admin-market-data/admin-market-data.html
  5. 121
      apps/client/src/app/components/admin-market-data/asset-profile-dialog/asset-profile-dialog.html
  6. 16
      apps/client/src/app/components/admin-overview/admin-overview.html
  7. 28
      apps/client/src/app/components/admin-users/admin-users.html
  8. 3
      apps/client/src/app/components/benchmark-comparator/benchmark-comparator.component.ts
  9. 170
      apps/client/src/app/components/home-overview/home-overview.html
  10. 4
      apps/client/src/app/components/home-summary/home-summary.html
  11. 3
      apps/client/src/app/components/investment-chart/investment-chart.component.ts
  12. 3
      apps/client/src/app/components/portfolio-performance/portfolio-performance.component.ts
  13. 4
      apps/client/src/app/components/portfolio-summary/portfolio-summary.component.ts
  14. 122
      apps/client/src/app/components/position/position-detail-dialog/position-detail-dialog.html
  15. 3
      apps/client/src/app/components/position/position.component.ts
  16. 3
      apps/client/src/app/components/positions/positions.component.ts
  17. 50
      apps/client/src/app/components/user-account-access/create-or-update-access-dialog/create-or-update-access-dialog.html
  18. 14
      apps/client/src/app/components/user-account-membership/user-account-membership.html
  19. 26
      apps/client/src/app/components/user-account-settings/user-account-settings.html
  20. 4
      apps/client/src/app/components/world-map-chart/world-map-chart.component.ts
  21. 2
      apps/client/src/app/pages/about/about-page.html
  22. 42
      apps/client/src/app/pages/about/oss-friends/oss-friends-page.html
  23. 18
      apps/client/src/app/pages/about/overview/about-overview-page.html
  24. 12
      apps/client/src/app/pages/accounts/accounts-page.html
  25. 48
      apps/client/src/app/pages/accounts/create-or-update-account-dialog/create-or-update-account-dialog.html
  26. 2
      apps/client/src/app/pages/admin/admin-page.html
  27. 92
      apps/client/src/app/pages/blog/blog-page.html
  28. 2
      apps/client/src/app/pages/faq/faq-page.html
  29. 58
      apps/client/src/app/pages/features/features-page.html
  30. 2
      apps/client/src/app/pages/home/home-page.html
  31. 3
      apps/client/src/app/pages/landing/landing-page.html
  32. 35
      apps/client/src/app/pages/portfolio/activities/activities-page.html
  33. 122
      apps/client/src/app/pages/portfolio/activities/create-or-update-activity-dialog/create-or-update-activity-dialog.html
  34. 279
      apps/client/src/app/pages/portfolio/activities/import-activities-dialog/import-activities-dialog.html
  35. 15
      apps/client/src/app/pages/portfolio/allocations/allocations-page.html
  36. 213
      apps/client/src/app/pages/portfolio/analysis/analysis-page.html
  37. 7
      apps/client/src/app/pages/portfolio/fire/fire-page.html
  38. 18
      apps/client/src/app/pages/portfolio/holdings/holdings-page.html
  39. 2
      apps/client/src/app/pages/portfolio/portfolio-page.html
  40. 26
      apps/client/src/app/pages/pricing/pricing-page.html
  41. 6
      apps/client/src/app/pages/register/show-access-token-dialog/show-access-token-dialog.html
  42. 54
      apps/client/src/app/pages/resources/personal-finance-tools/personal-finance-tools-page.html
  43. 42
      apps/client/src/app/pages/resources/personal-finance-tools/product-page-template.html
  44. 26
      apps/client/src/app/pages/resources/resources-page.html
  45. 2
      apps/client/src/app/pages/user-account/user-account-page.html
  46. 42
      apps/client/src/app/pages/webauthn/webauthn-page.html
  47. 2
      apps/client/src/app/pages/zen/zen-page.html
  48. 3
      libs/common/src/lib/chart-helper.ts
  49. 6
      libs/common/src/lib/helper.ts
  50. 3
      libs/ui/src/lib/account-balances/account-balances.component.ts
  51. 4
      libs/ui/src/lib/activities-table/activities-table.component.ts
  52. 9
      libs/ui/src/lib/assistant/assistant-list-item/assistant-list-item.html
  53. 46
      libs/ui/src/lib/assistant/assistant.html
  54. 4
      libs/ui/src/lib/benchmark/benchmark.component.ts
  55. 3
      libs/ui/src/lib/fire-calculator/fire-calculator.component.ts
  56. 3
      libs/ui/src/lib/holdings-table/holdings-table.component.ts
  57. 13
      libs/ui/src/lib/line-chart/line-chart.component.ts
  58. 4
      libs/ui/src/lib/portfolio-proportion-chart/portfolio-proportion-chart.component.ts
  59. 7
      libs/ui/src/lib/value/value.component.ts

1
CHANGELOG.md

@ -10,6 +10,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
### Changed ### Changed
- Improved the validation for `currency` in various endpoints - Improved the validation for `currency` in various endpoints
- Harmonized the setting of a default locale in various components
- Set the parser to `angular` in the `prettier` options - Set the parser to `angular` in the `prettier` options
## 2.54.0 - 2024-02-19 ## 2.54.0 - 2024-02-19

14
apps/client/src/app/components/account-detail-dialog/account-detail-dialog.html

@ -25,7 +25,9 @@
class="h-100" class="h-100"
[currency]="user?.settings?.baseCurrency" [currency]="user?.settings?.baseCurrency"
[historicalDataItems]="historicalDataItems" [historicalDataItems]="historicalDataItems"
[isInPercent]="data.hasImpersonationId || user.settings.isRestrictedView" [isInPercent]="
data.hasImpersonationId || user.settings.isRestrictedView
"
[isLoading]="isLoadingChart" [isLoading]="isLoadingChart"
[locale]="user?.settings?.locale" [locale]="user?.settings?.locale"
/> />
@ -92,7 +94,9 @@
[dataSource]="dataSource" [dataSource]="dataSource"
[deviceType]="data.deviceType" [deviceType]="data.deviceType"
[hasPermissionToCreateActivity]="false" [hasPermissionToCreateActivity]="false"
[hasPermissionToExportActivities]="!data.hasImpersonationId && !user.settings.isRestrictedView" [hasPermissionToExportActivities]="
!data.hasImpersonationId && !user.settings.isRestrictedView
"
[hasPermissionToFilter]="false" [hasPermissionToFilter]="false"
[hasPermissionToOpenDetails]="false" [hasPermissionToOpenDetails]="false"
[locale]="user?.settings?.locale" [locale]="user?.settings?.locale"
@ -113,7 +117,11 @@
[accountBalances]="accountBalances" [accountBalances]="accountBalances"
[accountId]="data.accountId" [accountId]="data.accountId"
[locale]="user?.settings?.locale" [locale]="user?.settings?.locale"
[showActions]="!data.hasImpersonationId && hasPermissionToDeleteAccountBalance && !user.settings.isRestrictedView" [showActions]="
!data.hasImpersonationId &&
hasPermissionToDeleteAccountBalance &&
!user.settings.isRestrictedView
"
(accountBalanceDeleted)="onDeleteAccountBalance($event)" (accountBalanceDeleted)="onDeleteAccountBalance($event)"
/> />
</mat-tab> </mat-tab>

4
apps/client/src/app/components/accounts-table/accounts-table.component.ts

@ -1,3 +1,5 @@
import { getLocale } from '@ghostfolio/common/helper';
import { import {
ChangeDetectionStrategy, ChangeDetectionStrategy,
Component, Component,
@ -27,7 +29,7 @@ export class AccountsTableComponent implements OnChanges, OnDestroy, OnInit {
@Input() baseCurrency: string; @Input() baseCurrency: string;
@Input() deviceType: string; @Input() deviceType: string;
@Input() hasPermissionToOpenDetails = true; @Input() hasPermissionToOpenDetails = true;
@Input() locale: string; @Input() locale = getLocale();
@Input() showActions: boolean; @Input() showActions: boolean;
@Input() showBalance = true; @Input() showBalance = true;
@Input() showFooter = true; @Input() showFooter = true;

25
apps/client/src/app/components/admin-market-data/admin-market-data.html

@ -163,7 +163,12 @@
<mat-menu #assetProfileActionsMenu="matMenu" xPosition="before"> <mat-menu #assetProfileActionsMenu="matMenu" xPosition="before">
<button <button
mat-menu-item mat-menu-item
(click)="onOpenAssetProfileDialog({ dataSource: element.dataSource, symbol: element.symbol })" (click)="
onOpenAssetProfileDialog({
dataSource: element.dataSource,
symbol: element.symbol
})
"
> >
<span class="align-items-center d-flex"> <span class="align-items-center d-flex">
<ion-icon class="mr-2" name="create-outline" /> <ion-icon class="mr-2" name="create-outline" />
@ -173,7 +178,12 @@
<button <button
mat-menu-item mat-menu-item
[disabled]="element.activitiesCount !== 0" [disabled]="element.activitiesCount !== 0"
(click)="onDeleteProfileData({dataSource: element.dataSource, symbol: element.symbol})" (click)="
onDeleteProfileData({
dataSource: element.dataSource,
symbol: element.symbol
})
"
> >
<span class="align-items-center d-flex"> <span class="align-items-center d-flex">
<ion-icon class="mr-2" name="trash-outline" /> <ion-icon class="mr-2" name="trash-outline" />
@ -189,16 +199,19 @@
*matRowDef="let row; columns: displayedColumns" *matRowDef="let row; columns: displayedColumns"
class="cursor-pointer" class="cursor-pointer"
mat-row mat-row
(click)="onOpenAssetProfileDialog({ dataSource: row.dataSource, symbol: row.symbol })" (click)="
onOpenAssetProfileDialog({
dataSource: row.dataSource,
symbol: row.symbol
})
"
></tr> ></tr>
</table> </table>
<mat-paginator <mat-paginator
[length]="totalItems" [length]="totalItems"
[ngClass]="{ [ngClass]="{
'd-none': 'd-none': (isLoading && totalItems === 0) || totalItems <= pageSize
(isLoading && totalItems === 0) ||
totalItems <= pageSize
}" }"
[pageSize]="pageSize" [pageSize]="pageSize"
[showFirstLastButtons]="true" [showFirstLastButtons]="true"

121
apps/client/src/app/components/admin-market-data/asset-profile-dialog/asset-profile-dialog.html

@ -25,7 +25,9 @@
mat-menu-item mat-menu-item
type="button" type="button"
[disabled]="assetProfileForm.dirty" [disabled]="assetProfileForm.dirty"
(click)="onGatherSymbol({dataSource: data.dataSource, symbol: data.symbol})" (click)="
onGatherSymbol({ dataSource: data.dataSource, symbol: data.symbol })
"
> >
<ng-container i18n>Gather Historical Data</ng-container> <ng-container i18n>Gather Historical Data</ng-container>
</button> </button>
@ -33,7 +35,12 @@
mat-menu-item mat-menu-item
type="button" type="button"
[disabled]="assetProfileForm.dirty" [disabled]="assetProfileForm.dirty"
(click)="onGatherProfileDataBySymbol({dataSource: data.dataSource, symbol: data.symbol})" (click)="
onGatherProfileDataBySymbol({
dataSource: data.dataSource,
symbol: data.symbol
})
"
> >
<ng-container i18n>Gather Profile Data</ng-container> <ng-container i18n>Gather Profile Data</ng-container>
</button> </button>
@ -73,7 +80,12 @@
color="accent" color="accent"
mat-flat-button mat-flat-button
type="button" type="button"
[disabled]="!assetProfileForm.controls['historicalData']?.controls['csvString'].touched || assetProfileForm.controls['historicalData']?.controls['csvString']?.value === ''" [disabled]="
!assetProfileForm.controls['historicalData']?.controls['csvString']
.touched ||
assetProfileForm.controls['historicalData']?.controls['csvString']
?.value === ''
"
(click)="onImportHistoricalData()" (click)="onImportHistoricalData()"
> >
<ng-container i18n>Import</ng-container> <ng-container i18n>Import</ng-container>
@ -129,49 +141,54 @@
> >
</div> </div>
<ng-container <ng-container
*ngIf="assetProfile?.countries?.length > 0 || assetProfile?.sectors?.length > 0" *ngIf="
assetProfile?.countries?.length > 0 ||
assetProfile?.sectors?.length > 0
"
> >
@if (assetProfile?.countries?.length === 1 && @if (
assetProfile?.sectors?.length === 1 ) { assetProfile?.countries?.length === 1 &&
<div *ngIf="assetProfile?.sectors?.length === 1" class="col-6 mb-3"> assetProfile?.sectors?.length === 1
<gf-value ) {
i18n <div *ngIf="assetProfile?.sectors?.length === 1" class="col-6 mb-3">
size="medium" <gf-value
[locale]="data.locale" i18n
[value]="assetProfile?.sectors[0].name" size="medium"
>Sector</gf-value [locale]="data.locale"
> [value]="assetProfile?.sectors[0].name"
</div> >Sector</gf-value
<div *ngIf="assetProfile?.countries?.length === 1" class="col-6 mb-3"> >
<gf-value </div>
i18n <div *ngIf="assetProfile?.countries?.length === 1" class="col-6 mb-3">
size="medium" <gf-value
[locale]="data.locale" i18n
[value]="assetProfile?.countries[0].name" size="medium"
>Country</gf-value [locale]="data.locale"
> [value]="assetProfile?.countries[0].name"
</div> >Country</gf-value
>
</div>
} @else { } @else {
<div class="col-md-6 mb-3"> <div class="col-md-6 mb-3">
<div class="h5" i18n>Sectors</div> <div class="h5" i18n>Sectors</div>
<gf-portfolio-proportion-chart <gf-portfolio-proportion-chart
[colorScheme]="data.colorScheme" [colorScheme]="data.colorScheme"
[isInPercent]="true" [isInPercent]="true"
[keys]="['name']" [keys]="['name']"
[maxItems]="10" [maxItems]="10"
[positions]="sectors" [positions]="sectors"
/> />
</div> </div>
<div class="col-md-6 mb-3"> <div class="col-md-6 mb-3">
<div class="h5" i18n>Countries</div> <div class="h5" i18n>Countries</div>
<gf-portfolio-proportion-chart <gf-portfolio-proportion-chart
[colorScheme]="data.colorScheme" [colorScheme]="data.colorScheme"
[isInPercent]="true" [isInPercent]="true"
[keys]="['name']" [keys]="['name']"
[maxItems]="10" [maxItems]="10"
[positions]="countries" [positions]="countries"
/> />
</div> </div>
} }
</ng-container> </ng-container>
</div> </div>
@ -222,7 +239,17 @@
color="primary" color="primary"
i18n i18n
[checked]="isBenchmark" [checked]="isBenchmark"
(change)="isBenchmark ? onUnsetBenchmark({dataSource: data.dataSource, symbol: data.symbol}) : onSetBenchmark({dataSource: data.dataSource, symbol: data.symbol})" (change)="
isBenchmark
? onUnsetBenchmark({
dataSource: data.dataSource,
symbol: data.symbol
})
: onSetBenchmark({
dataSource: data.dataSource,
symbol: data.symbol
})
"
>Benchmark</mat-checkbox >Benchmark</mat-checkbox
> >
</div> </div>
@ -253,7 +280,9 @@
color="accent" color="accent"
mat-flat-button mat-flat-button
type="button" type="button"
[disabled]="assetProfileForm.controls['scraperConfiguration'].value === '{}'" [disabled]="
assetProfileForm.controls['scraperConfiguration'].value === '{}'
"
(click)="onTestMarketData()" (click)="onTestMarketData()"
> >
<ng-container i18n>Test</ng-container> <ng-container i18n>Test</ng-container>

16
apps/client/src/app/components/admin-overview/admin-overview.html

@ -28,7 +28,7 @@
[value]="transactionCount" [value]="transactionCount"
/> />
<div *ngIf="transactionCount && userCount"> <div *ngIf="transactionCount && userCount">
{{ transactionCount / userCount | number : '1.2-2' }} {{ transactionCount / userCount | number: '1.2-2' }}
<span i18n>per User</span> <span i18n>per User</span>
</div> </div>
</div> </div>
@ -69,10 +69,10 @@
<a <a
mat-menu-item mat-menu-item
[queryParams]="{ [queryParams]="{
assetProfileDialog: true, assetProfileDialog: true,
dataSource: exchangeRate.dataSource, dataSource: exchangeRate.dataSource,
symbol: exchangeRate.symbol symbol: exchangeRate.symbol
}" }"
[routerLink]="['/admin', 'market-data']" [routerLink]="['/admin', 'market-data']"
> >
<span class="align-items-center d-flex"> <span class="align-items-center d-flex">
@ -112,7 +112,9 @@
<mat-slide-toggle <mat-slide-toggle
color="primary" color="primary"
hideIcon="true" hideIcon="true"
[checked]="info.globalPermissions.includes(permissions.createUserAccount)" [checked]="
info.globalPermissions.includes(permissions.createUserAccount)
"
(change)="onEnableUserSignupModeChange($event)" (change)="onEnableUserSignupModeChange($event)"
/> />
</div> </div>
@ -143,7 +145,7 @@
<div class="w-50" i18n>System Message</div> <div class="w-50" i18n>System Message</div>
<div class="w-50"> <div class="w-50">
<div *ngIf="systemMessage" class="align-items-center d-flex"> <div *ngIf="systemMessage" class="align-items-center d-flex">
<div class="text-truncate">{{ systemMessage | json }}</div> <div class="text-truncate">{{ systemMessage | json }}</div>
<button <button
class="h-100 mx-1 no-min-width px-2" class="h-100 mx-1 no-min-width px-2"
mat-button mat-button

28
apps/client/src/app/components/admin-users/admin-users.html

@ -12,7 +12,7 @@
# #
</th> </th>
<td <td
*matCellDef="let element; let i=index" *matCellDef="let element; let i = index"
class="mat-mdc-cell px-1 py-2 text-right" class="mat-mdc-cell px-1 py-2 text-right"
mat-cell mat-cell
> >
@ -35,17 +35,23 @@
mat-cell mat-cell
> >
<div class="d-flex align-items-center"> <div class="d-flex align-items-center">
<span class="d-none d-sm-inline-block text-monospace" <span class="d-none d-sm-inline-block text-monospace">{{
>{{ element.id }}</span element.id
> }}</span>
<span class="d-inline-block d-sm-none text-monospace" <span class="d-inline-block d-sm-none text-monospace">{{
>{{ (element.id | slice:0:5) + '...' }}</span (element.id | slice: 0 : 5) + '...'
> }}</span>
<gf-premium-indicator <gf-premium-indicator
*ngIf="element?.subscription?.type === 'Premium'" *ngIf="element?.subscription?.type === 'Premium'"
class="ml-1" class="ml-1"
[enableLink]="false" [enableLink]="false"
[title]="'Expires ' + formatDistanceToNow(element.subscription.expiresAt) + ' (' + (element.subscription.expiresAt | date: defaultDateFormat) + ')'" [title]="
'Expires ' +
formatDistanceToNow(element.subscription.expiresAt) +
' (' +
(element.subscription.expiresAt | date: defaultDateFormat) +
')'
"
/> />
</div> </div>
</td> </td>
@ -67,9 +73,9 @@
class="mat-mdc-cell px-1 py-2" class="mat-mdc-cell px-1 py-2"
mat-cell mat-cell
> >
<span class="h5" [title]="element.country" <span class="h5" [title]="element.country">{{
>{{ getEmojiFlag(element.country) }}</span getEmojiFlag(element.country)
> }}</span>
</td> </td>
</ng-container> </ng-container>

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

@ -7,6 +7,7 @@ import { primaryColorRgb, secondaryColorRgb } from '@ghostfolio/common/config';
import { import {
getBackgroundColor, getBackgroundColor,
getDateFormatString, getDateFormatString,
getLocale,
getTextColor, getTextColor,
parseDate parseDate
} from '@ghostfolio/common/helper'; } from '@ghostfolio/common/helper';
@ -51,7 +52,7 @@ export class BenchmarkComparatorComponent implements OnChanges, OnDestroy {
@Input() colorScheme: ColorScheme; @Input() colorScheme: ColorScheme;
@Input() daysInMarket: number; @Input() daysInMarket: number;
@Input() isLoading: boolean; @Input() isLoading: boolean;
@Input() locale: string; @Input() locale = getLocale();
@Input() performanceDataItems: LineChartItem[]; @Input() performanceDataItems: LineChartItem[];
@Input() user: User; @Input() user: User;

170
apps/client/src/app/components/home-overview/home-overview.html

@ -1,100 +1,100 @@
<div <div
class="align-items-center container d-flex flex-column h-100 justify-content-center overview p-0 position-relative" class="align-items-center container d-flex flex-column h-100 justify-content-center overview p-0 position-relative"
> >
@if(hasPermissionToCreateOrder && historicalDataItems?.length === 0) { @if (hasPermissionToCreateOrder && historicalDataItems?.length === 0) {
<div class="justify-content-center row w-100"> <div class="justify-content-center row w-100">
<div class="col introduction"> <div class="col introduction">
<h4 i18n>Welcome to Ghostfolio</h4> <h4 i18n>Welcome to Ghostfolio</h4>
<p i18n>Ready to take control of your personal finances?</p> <p i18n>Ready to take control of your personal finances?</p>
<ol class="font-weight-bold"> <ol class="font-weight-bold">
<li <li
class="mb-2" class="mb-2"
[ngClass]="{ 'text-muted': user?.accounts?.length > 1 }" [ngClass]="{ 'text-muted': user?.accounts?.length > 1 }"
>
<a class="d-block" [routerLink]="['/accounts']"
><span i18n>Setup your accounts</span><br />
<span class="font-weight-normal" i18n
>Get a comprehensive financial overview by adding your bank and
brokerage accounts.</span
></a
> >
</li> <a class="d-block" [routerLink]="['/accounts']"
<li class="mb-2"> ><span i18n>Setup your accounts</span><br />
<a class="d-block" [routerLink]="['/portfolio', 'activities']"> <span class="font-weight-normal" i18n
<span i18n>Capture your activities</span><br /> >Get a comprehensive financial overview by adding your bank and
<span class="font-weight-normal" i18n brokerage accounts.</span
>Record your investment activities to keep your portfolio up to ></a
date.</span >
></a </li>
> <li class="mb-2">
</li> <a class="d-block" [routerLink]="['/portfolio', 'activities']">
<li class="mb-2"> <span i18n>Capture your activities</span><br />
<a class="d-block" [routerLink]="['/portfolio']"> <span class="font-weight-normal" i18n
<span i18n>Monitor and analyze your portfolio</span><br /> >Record your investment activities to keep your portfolio up to
<span class="font-weight-normal" i18n date.</span
>Track your progress in real-time with comprehensive analysis and ></a
insights.</span
> >
</li>
<li class="mb-2">
<a class="d-block" [routerLink]="['/portfolio']">
<span i18n>Monitor and analyze your portfolio</span><br />
<span class="font-weight-normal" i18n
>Track your progress in real-time with comprehensive analysis
and insights.</span
>
</a>
</li>
</ol>
<div class="d-flex justify-content-center">
<a
*ngIf="user?.accounts?.length === 1"
color="primary"
mat-flat-button
[routerLink]="['/accounts']"
>
<ng-container i18n>Setup accounts</ng-container>
</a>
<a
*ngIf="user?.accounts?.length > 1"
color="primary"
mat-flat-button
[routerLink]="['/portfolio', 'activities']"
>
<ng-container i18n>Add activity</ng-container>
</a> </a>
</li> </div>
</ol>
<div class="d-flex justify-content-center">
<a
*ngIf="user?.accounts?.length === 1"
color="primary"
mat-flat-button
[routerLink]="['/accounts']"
>
<ng-container i18n>Setup accounts</ng-container>
</a>
<a
*ngIf="user?.accounts?.length > 1"
color="primary"
mat-flat-button
[routerLink]="['/portfolio', 'activities']"
>
<ng-container i18n>Add activity</ng-container>
</a>
</div> </div>
</div> </div>
</div>
} @else { } @else {
<div class="row w-100"> <div class="row w-100">
<div class="col p-0"> <div class="col p-0">
<div class="chart-container mx-auto position-relative"> <div class="chart-container mx-auto position-relative">
<gf-line-chart <gf-line-chart
class="position-absolute" class="position-absolute"
symbol="Performance" symbol="Performance"
unit="%" unit="%"
[colorScheme]="user?.settings?.colorScheme" [colorScheme]="user?.settings?.colorScheme"
[hidden]="historicalDataItems?.length === 0" [hidden]="historicalDataItems?.length === 0"
[historicalDataItems]="historicalDataItems" [historicalDataItems]="historicalDataItems"
[isAnimated]="user?.settings?.dateRange === '1d' ? false : true" [isAnimated]="user?.settings?.dateRange === '1d' ? false : true"
[locale]="user?.settings?.locale"
[ngClass]="{ 'pr-3': deviceType === 'mobile' }"
[showGradient]="true"
[showLoader]="false"
[showXAxis]="false"
[showYAxis]="false"
/>
</div>
</div>
</div>
<div class="overview-container row mt-1">
<div class="col">
<gf-portfolio-performance
class="pb-4"
[deviceType]="deviceType"
[errors]="errors"
[isAllTimeHigh]="isAllTimeHigh"
[isAllTimeLow]="isAllTimeLow"
[isLoading]="isLoadingPerformance"
[locale]="user?.settings?.locale" [locale]="user?.settings?.locale"
[ngClass]="{ 'pr-3': deviceType === 'mobile' }" [performance]="performance"
[showGradient]="true" [showDetails]="showDetails"
[showLoader]="false" [unit]="unit"
[showXAxis]="false"
[showYAxis]="false"
/> />
</div> </div>
</div> </div>
</div>
<div class="overview-container row mt-1">
<div class="col">
<gf-portfolio-performance
class="pb-4"
[deviceType]="deviceType"
[errors]="errors"
[isAllTimeHigh]="isAllTimeHigh"
[isAllTimeLow]="isAllTimeLow"
[isLoading]="isLoadingPerformance"
[locale]="user?.settings?.locale"
[performance]="performance"
[showDetails]="showDetails"
[unit]="unit"
/>
</div>
</div>
} }
</div> </div>

4
apps/client/src/app/components/home-summary/home-summary.html

@ -6,7 +6,9 @@
<mat-card-content> <mat-card-content>
<gf-portfolio-summary <gf-portfolio-summary
[baseCurrency]="user?.settings?.baseCurrency" [baseCurrency]="user?.settings?.baseCurrency"
[hasPermissionToUpdateUserSettings]="!hasImpersonationId && hasPermissionToUpdateUserSettings" [hasPermissionToUpdateUserSettings]="
!hasImpersonationId && hasPermissionToUpdateUserSettings
"
[isLoading]="isLoading" [isLoading]="isLoading"
[language]="user?.settings?.language" [language]="user?.settings?.language"
[locale]="user?.settings?.locale" [locale]="user?.settings?.locale"

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

@ -9,6 +9,7 @@ import {
DATE_FORMAT, DATE_FORMAT,
getBackgroundColor, getBackgroundColor,
getDateFormatString, getDateFormatString,
getLocale,
getTextColor, getTextColor,
parseDate parseDate
} from '@ghostfolio/common/helper'; } from '@ghostfolio/common/helper';
@ -65,7 +66,7 @@ export class InvestmentChartComponent implements OnChanges, OnDestroy {
@Input() historicalDataItems: LineChartItem[] = []; @Input() historicalDataItems: LineChartItem[] = [];
@Input() isInPercent = false; @Input() isInPercent = false;
@Input() isLoading = false; @Input() isLoading = false;
@Input() locale: string; @Input() locale = getLocale();
@Input() range: DateRange = 'max'; @Input() range: DateRange = 'max';
@Input() savingsRate = 0; @Input() savingsRate = 0;

3
apps/client/src/app/components/portfolio-performance/portfolio-performance.component.ts

@ -1,4 +1,5 @@
import { import {
getLocale,
getNumberFormatDecimal, getNumberFormatDecimal,
getNumberFormatGroup getNumberFormatGroup
} from '@ghostfolio/common/helper'; } from '@ghostfolio/common/helper';
@ -31,7 +32,7 @@ export class PortfolioPerformanceComponent implements OnChanges, OnInit {
@Input() isAllTimeHigh: boolean; @Input() isAllTimeHigh: boolean;
@Input() isAllTimeLow: boolean; @Input() isAllTimeLow: boolean;
@Input() isLoading: boolean; @Input() isLoading: boolean;
@Input() locale: string; @Input() locale = getLocale();
@Input() performance: PortfolioPerformance; @Input() performance: PortfolioPerformance;
@Input() showDetails: boolean; @Input() showDetails: boolean;
@Input() unit: string; @Input() unit: string;

4
apps/client/src/app/components/portfolio-summary/portfolio-summary.component.ts

@ -1,4 +1,4 @@
import { getDateFnsLocale } from '@ghostfolio/common/helper'; import { getDateFnsLocale, getLocale } from '@ghostfolio/common/helper';
import { PortfolioSummary } from '@ghostfolio/common/interfaces'; import { PortfolioSummary } from '@ghostfolio/common/interfaces';
import { import {
@ -23,7 +23,7 @@ export class PortfolioSummaryComponent implements OnChanges, OnInit {
@Input() hasPermissionToUpdateUserSettings: boolean; @Input() hasPermissionToUpdateUserSettings: boolean;
@Input() isLoading: boolean; @Input() isLoading: boolean;
@Input() language: string; @Input() language: string;
@Input() locale: string; @Input() locale = getLocale();
@Input() summary: PortfolioSummary; @Input() summary: PortfolioSummary;
@Output() emergencyFundChanged = new EventEmitter<number>(); @Output() emergencyFundChanged = new EventEmitter<number>();

122
apps/client/src/app/components/position/position-detail-dialog/position-detail-dialog.html

@ -87,7 +87,11 @@
size="medium" size="medium"
[isCurrency]="true" [isCurrency]="true"
[locale]="data.locale" [locale]="data.locale"
[ngClass]="{ 'text-danger': minPrice?.toFixed(2) === marketPrice?.toFixed(2) && maxPrice?.toFixed(2) !== minPrice?.toFixed(2) }" [ngClass]="{
'text-danger':
minPrice?.toFixed(2) === marketPrice?.toFixed(2) &&
maxPrice?.toFixed(2) !== minPrice?.toFixed(2)
}"
[unit]="SymbolProfile?.currency" [unit]="SymbolProfile?.currency"
[value]="minPrice" [value]="minPrice"
>Minimum Price</gf-value >Minimum Price</gf-value
@ -99,7 +103,11 @@
size="medium" size="medium"
[isCurrency]="true" [isCurrency]="true"
[locale]="data.locale" [locale]="data.locale"
[ngClass]="{ 'text-success': maxPrice?.toFixed(2) === marketPrice?.toFixed(2) && maxPrice?.toFixed(2) !== minPrice?.toFixed(2) }" [ngClass]="{
'text-success':
maxPrice?.toFixed(2) === marketPrice?.toFixed(2) &&
maxPrice?.toFixed(2) !== minPrice?.toFixed(2)
}"
[unit]="SymbolProfile?.currency" [unit]="SymbolProfile?.currency"
[value]="maxPrice" [value]="maxPrice"
>Maximum Price</gf-value >Maximum Price</gf-value
@ -184,53 +192,61 @@
> >
</div> </div>
<ng-container <ng-container
*ngIf="SymbolProfile?.countries?.length > 0 || SymbolProfile?.sectors?.length > 0" *ngIf="
SymbolProfile?.countries?.length > 0 ||
SymbolProfile?.sectors?.length > 0
"
> >
@if(SymbolProfile?.countries?.length === 1 && @if (
SymbolProfile?.sectors?.length === 1) { SymbolProfile?.countries?.length === 1 &&
<div *ngIf="SymbolProfile?.sectors?.length === 1" class="col-6 mb-3"> SymbolProfile?.sectors?.length === 1
<gf-value ) {
i18n <div *ngIf="SymbolProfile?.sectors?.length === 1" class="col-6 mb-3">
size="medium" <gf-value
[locale]="data.locale" i18n
[value]="SymbolProfile.sectors[0].name" size="medium"
>Sector</gf-value [locale]="data.locale"
[value]="SymbolProfile.sectors[0].name"
>Sector</gf-value
>
</div>
<div
*ngIf="SymbolProfile?.countries?.length === 1"
class="col-6 mb-3"
> >
</div> <gf-value
<div *ngIf="SymbolProfile?.countries?.length === 1" class="col-6 mb-3"> i18n
<gf-value size="medium"
i18n [locale]="data.locale"
size="medium" [value]="SymbolProfile.countries[0].name"
[locale]="data.locale" >Country</gf-value
[value]="SymbolProfile.countries[0].name" >
>Country</gf-value </div>
>
</div>
} @else { } @else {
<div class="col-md-6 mb-3"> <div class="col-md-6 mb-3">
<div class="h5" i18n>Sectors</div> <div class="h5" i18n>Sectors</div>
<gf-portfolio-proportion-chart <gf-portfolio-proportion-chart
[baseCurrency]="data.baseCurrency" [baseCurrency]="data.baseCurrency"
[colorScheme]="data.colorScheme" [colorScheme]="data.colorScheme"
[isInPercent]="true" [isInPercent]="true"
[keys]="['name']" [keys]="['name']"
[locale]="data.locale" [locale]="data.locale"
[maxItems]="10" [maxItems]="10"
[positions]="sectors" [positions]="sectors"
/> />
</div> </div>
<div class="col-md-6 mb-3"> <div class="col-md-6 mb-3">
<div class="h5" i18n>Countries</div> <div class="h5" i18n>Countries</div>
<gf-portfolio-proportion-chart <gf-portfolio-proportion-chart
[baseCurrency]="data.baseCurrency" [baseCurrency]="data.baseCurrency"
[colorScheme]="data.colorScheme" [colorScheme]="data.colorScheme"
[isInPercent]="true" [isInPercent]="true"
[keys]="['name']" [keys]="['name']"
[locale]="data.locale" [locale]="data.locale"
[maxItems]="10" [maxItems]="10"
[positions]="countries" [positions]="countries"
/> />
</div> </div>
} }
</ng-container> </ng-container>
<div *ngIf="dataProviderInfo" class="col-md-12 mb-3 text-center"> <div *ngIf="dataProviderInfo" class="col-md-12 mb-3 text-center">
@ -257,7 +273,9 @@
[dataSource]="dataSource" [dataSource]="dataSource"
[deviceType]="data.deviceType" [deviceType]="data.deviceType"
[hasPermissionToCreateActivity]="false" [hasPermissionToCreateActivity]="false"
[hasPermissionToExportActivities]="!data.hasImpersonationId && !user.settings.isRestrictedView" [hasPermissionToExportActivities]="
!data.hasImpersonationId && !user.settings.isRestrictedView
"
[hasPermissionToFilter]="false" [hasPermissionToFilter]="false"
[hasPermissionToOpenDetails]="false" [hasPermissionToOpenDetails]="false"
[locale]="data.locale" [locale]="data.locale"
@ -294,15 +312,17 @@
<div class="col"> <div class="col">
<div class="h5" i18n>Tags</div> <div class="h5" i18n>Tags</div>
<mat-chip-listbox> <mat-chip-listbox>
<mat-chip-option *ngFor="let tag of tags" disabled <mat-chip-option *ngFor="let tag of tags" disabled>{{
>{{ tag.name }}</mat-chip-option tag.name
> }}</mat-chip-option>
</mat-chip-listbox> </mat-chip-listbox>
</div> </div>
</div> </div>
<div <div
*ngIf="activities?.length > 0 && data.hasPermissionToReportDataGlitch === true" *ngIf="
activities?.length > 0 && data.hasPermissionToReportDataGlitch === true
"
class="row" class="row"
> >
<div class="col"> <div class="col">

3
apps/client/src/app/components/position/position.component.ts

@ -1,4 +1,5 @@
import { UNKNOWN_KEY } from '@ghostfolio/common/config'; import { UNKNOWN_KEY } from '@ghostfolio/common/config';
import { getLocale } from '@ghostfolio/common/helper';
import { Position } from '@ghostfolio/common/interfaces'; import { Position } from '@ghostfolio/common/interfaces';
import { import {
@ -20,7 +21,7 @@ export class PositionComponent implements OnDestroy, OnInit {
@Input() baseCurrency: string; @Input() baseCurrency: string;
@Input() deviceType: string; @Input() deviceType: string;
@Input() isLoading: boolean; @Input() isLoading: boolean;
@Input() locale: string; @Input() locale = getLocale();
@Input() position: Position; @Input() position: Position;
@Input() range: string; @Input() range: string;

3
apps/client/src/app/components/positions/positions.component.ts

@ -1,3 +1,4 @@
import { getLocale } from '@ghostfolio/common/helper';
import { Position } from '@ghostfolio/common/interfaces'; import { Position } from '@ghostfolio/common/interfaces';
import { import {
@ -18,7 +19,7 @@ export class PositionsComponent implements OnChanges, OnInit {
@Input() baseCurrency: string; @Input() baseCurrency: string;
@Input() deviceType: string; @Input() deviceType: string;
@Input() hasPermissionToCreateOrder: boolean; @Input() hasPermissionToCreateOrder: boolean;
@Input() locale: string; @Input() locale = getLocale();
@Input() positions: Position[]; @Input() positions: Position[];
@Input() range: string; @Input() range: string;

50
apps/client/src/app/components/user-account-access/create-or-update-access-dialog/create-or-update-access-dialog.html

@ -28,30 +28,32 @@
</div> </div>
@if (accessForm.controls['type'].value === 'PRIVATE') { @if (accessForm.controls['type'].value === 'PRIVATE') {
<div> <div>
<mat-form-field appearance="outline" class="w-100"> <mat-form-field appearance="outline" class="w-100">
<mat-label i18n>Permission</mat-label> <mat-label i18n>Permission</mat-label>
<mat-select formControlName="permissions"> <mat-select formControlName="permissions">
<mat-option i18n value="READ_RESTRICTED">Restricted view</mat-option> <mat-option i18n value="READ_RESTRICTED"
@if(data?.user?.settings?.isExperimentalFeatures) { >Restricted view</mat-option
<mat-option i18n value="READ">View</mat-option> >
} @if (data?.user?.settings?.isExperimentalFeatures) {
</mat-select> <mat-option i18n value="READ">View</mat-option>
</mat-form-field> }
</div> </mat-select>
<div> </mat-form-field>
<mat-form-field appearance="outline" class="w-100"> </div>
<mat-label> <div>
Ghostfolio <ng-container i18n>User ID</ng-container> <mat-form-field appearance="outline" class="w-100">
</mat-label> <mat-label>
<input Ghostfolio <ng-container i18n>User ID</ng-container>
formControlName="userId" </mat-label>
matInput <input
type="text" formControlName="userId"
(keydown.enter)="$event.stopPropagation()" matInput
/> type="text"
</mat-form-field> (keydown.enter)="$event.stopPropagation()"
</div> />
</mat-form-field>
</div>
} }
</div> </div>
<div class="justify-content-end" mat-dialog-actions> <div class="justify-content-end" mat-dialog-actions>

14
apps/client/src/app/components/user-account-membership/user-account-membership.html

@ -3,7 +3,7 @@
<div class="col"> <div class="col">
<div class="align-items-center d-flex flex-column"> <div class="align-items-center d-flex flex-column">
<gf-membership-card <gf-membership-card
[expiresAt]="user?.subscription?.expiresAt | date: defaultDateFormat" [expiresAt]="user?.subscription?.expiresAt | date: defaultDateFormat"
[name]="user?.subscription?.type" [name]="user?.subscription?.type"
/> />
<div <div
@ -11,14 +11,19 @@
class="d-flex flex-column mt-5" class="d-flex flex-column mt-5"
> >
<ng-container <ng-container
*ngIf="hasPermissionForSubscription && hasPermissionToUpdateUserSettings" *ngIf="
hasPermissionForSubscription && hasPermissionToUpdateUserSettings
"
> >
<button color="primary" mat-flat-button (click)="onCheckout()"> <button color="primary" mat-flat-button (click)="onCheckout()">
<ng-container *ngIf="user.subscription.offer === 'default'" i18n <ng-container *ngIf="user.subscription.offer === 'default'" i18n
>Upgrade Plan</ng-container >Upgrade Plan</ng-container
> >
<ng-container <ng-container
*ngIf="user.subscription.offer === 'renewal' || user.subscription.offer === 'renewal-early-bird'" *ngIf="
user.subscription.offer === 'renewal' ||
user.subscription.offer === 'renewal-early-bird'
"
i18n i18n
>Renew Plan</ng-container >Renew Plan</ng-container
> >
@ -27,7 +32,8 @@
<ng-container *ngIf="coupon" <ng-container *ngIf="coupon"
><del class="text-muted" ><del class="text-muted"
>{{ baseCurrency }}&nbsp;{{ price }}</del >{{ baseCurrency }}&nbsp;{{ price }}</del
>&nbsp;{{ baseCurrency }}&nbsp;{{ price - coupon >&nbsp;{{ baseCurrency }}&nbsp;{{
price - coupon
}}</ng-container }}</ng-container
> >
<ng-container *ngIf="!coupon" <ng-container *ngIf="!coupon"

26
apps/client/src/app/components/user-account-settings/user-account-settings.html

@ -32,7 +32,9 @@
name="baseCurrency" name="baseCurrency"
[disabled]="!hasPermissionToUpdateUserSettings" [disabled]="!hasPermissionToUpdateUserSettings"
[value]="user.settings.baseCurrency" [value]="user.settings.baseCurrency"
(selectionChange)="onChangeUserSetting('baseCurrency', $event.value)" (selectionChange)="
onChangeUserSetting('baseCurrency', $event.value)
"
> >
<mat-option <mat-option
*ngFor="let currency of currencies" *ngFor="let currency of currencies"
@ -53,7 +55,9 @@
> >
If a translation is missing, kindly support us in extending it If a translation is missing, kindly support us in extending it
<a <a
href="https://github.com/ghostfolio/ghostfolio/blob/main/apps/client/src/locales/messages.{{language}}.xlf" href="https://github.com/ghostfolio/ghostfolio/blob/main/apps/client/src/locales/messages.{{
language
}}.xlf"
target="_blank" target="_blank"
>here</a >here</a
>. >.
@ -65,7 +69,9 @@
name="language" name="language"
[disabled]="!hasPermissionToUpdateUserSettings" [disabled]="!hasPermissionToUpdateUserSettings"
[value]="language" [value]="language"
(selectionChange)="onChangeUserSetting('language', $event.value)" (selectionChange)="
onChangeUserSetting('language', $event.value)
"
> >
<mat-option [value]="null" /> <mat-option [value]="null" />
<mat-option value="de">Deutsch</mat-option> <mat-option value="de">Deutsch</mat-option>
@ -115,12 +121,14 @@
name="locale" name="locale"
[disabled]="!hasPermissionToUpdateUserSettings" [disabled]="!hasPermissionToUpdateUserSettings"
[value]="user.settings.locale" [value]="user.settings.locale"
(selectionChange)="onChangeUserSetting('locale', $event.value)" (selectionChange)="
onChangeUserSetting('locale', $event.value)
"
> >
<mat-option [value]="null" /> <mat-option [value]="null" />
<mat-option *ngFor="let locale of locales" [value]="locale" <mat-option *ngFor="let locale of locales" [value]="locale">{{
>{{ locale }}</mat-option locale
> }}</mat-option>
</mat-select> </mat-select>
</mat-form-field> </mat-form-field>
</div> </div>
@ -137,7 +145,9 @@
[disabled]="!hasPermissionToUpdateUserSettings" [disabled]="!hasPermissionToUpdateUserSettings"
[placeholder]="appearancePlaceholder" [placeholder]="appearancePlaceholder"
[value]="user?.settings?.colorScheme" [value]="user?.settings?.colorScheme"
(selectionChange)="onChangeUserSetting('colorScheme', $event.value)" (selectionChange)="
onChangeUserSetting('colorScheme', $event.value)
"
> >
<mat-option i18n [value]="null">Auto</mat-option> <mat-option i18n [value]="null">Auto</mat-option>
<mat-option i18n value="LIGHT">Light</mat-option> <mat-option i18n value="LIGHT">Light</mat-option>

4
apps/client/src/app/components/world-map-chart/world-map-chart.component.ts

@ -1,4 +1,4 @@
import { getNumberFormatGroup } from '@ghostfolio/common/helper'; import { getLocale, getNumberFormatGroup } from '@ghostfolio/common/helper';
import { import {
ChangeDetectionStrategy, ChangeDetectionStrategy,
@ -21,7 +21,7 @@ export class WorldMapChartComponent implements OnChanges, OnDestroy, OnInit {
@Input() countries: { [code: string]: { name?: string; value: number } }; @Input() countries: { [code: string]: { name?: string; value: number } };
@Input() format: string; @Input() format: string;
@Input() isInPercent = false; @Input() isInPercent = false;
@Input() locale: string; @Input() locale = getLocale();
public isLoading = true; public isLoading = true;
public svgMapElement; public svgMapElement;

2
apps/client/src/app/pages/about/about-page.html

@ -21,7 +21,7 @@
> >
<ion-icon <ion-icon
[name]="tab.iconName" [name]="tab.iconName"
[size]="deviceType === 'mobile' ? 'large': 'small'" [size]="deviceType === 'mobile' ? 'large' : 'small'"
/> />
<div class="d-none d-sm-block ml-2">{{ tab.label }}</div> <div class="d-none d-sm-block ml-2">{{ tab.label }}</div>
</a> </a>

42
apps/client/src/app/pages/about/oss-friends/oss-friends-page.html

@ -11,26 +11,28 @@
</h1> </h1>
<div class="row"> <div class="row">
@for (ossFriend of ossFriends; track ossFriend) { @for (ossFriend of ossFriends; track ossFriend) {
<div class="col-xs-12 col-md-4 mb-3"> <div class="col-xs-12 col-md-4 mb-3">
<a target="_blank" [href]="ossFriend.href"> <a target="_blank" [href]="ossFriend.href">
<mat-card appearance="outlined" class="d-flex flex-column h-100"> <mat-card appearance="outlined" class="d-flex flex-column h-100">
<mat-card-header> <mat-card-header>
<mat-card-title class="h4">{{ ossFriend.name }}</mat-card-title> <mat-card-title class="h4">{{
</mat-card-header> ossFriend.name
<mat-card-content class="flex-grow-1"> }}</mat-card-title>
<p>{{ ossFriend.description }}</p> </mat-card-header>
</mat-card-content> <mat-card-content class="flex-grow-1">
<mat-card-actions class="justify-content-end"> <p>{{ ossFriend.description }}</p>
<a mat-button target="_blank" [href]="ossFriend.href"> </mat-card-content>
<span <mat-card-actions class="justify-content-end">
><ng-container i18n>Visit</ng-container> {{ ossFriend.name <a mat-button target="_blank" [href]="ossFriend.href">
}}</span <span
><ion-icon class="ml-1" name="arrow-forward-outline" /> ><ng-container i18n>Visit</ng-container>
</a> {{ ossFriend.name }}</span
</mat-card-actions> ><ion-icon class="ml-1" name="arrow-forward-outline" />
</mat-card> </a>
</a> </mat-card-actions>
</div> </mat-card>
</a>
</div>
} }
</div> </div>
</div> </div>

18
apps/client/src/app/pages/about/overview/about-overview-page.html

@ -147,15 +147,15 @@
> >
</div> </div>
@if (hasPermissionForSubscription) { @if (hasPermissionForSubscription) {
<div class="col-md-6 col-xs-12 my-2"> <div class="col-md-6 col-xs-12 my-2">
<a <a
class="py-4 w-100" class="py-4 w-100"
color="primary" color="primary"
mat-flat-button mat-flat-button
[routerLink]="['/blog']" [routerLink]="['/blog']"
>Blog</a >Blog</a
> >
</div> </div>
} }
</div> </div>
</div> </div>

12
apps/client/src/app/pages/accounts/accounts-page.html

@ -8,7 +8,11 @@
[baseCurrency]="user?.settings?.baseCurrency" [baseCurrency]="user?.settings?.baseCurrency"
[deviceType]="deviceType" [deviceType]="deviceType"
[locale]="user?.settings?.locale" [locale]="user?.settings?.locale"
[showActions]="!hasImpersonationId && hasPermissionToUpdateAccount && !user.settings.isRestrictedView" [showActions]="
!hasImpersonationId &&
hasPermissionToUpdateAccount &&
!user.settings.isRestrictedView
"
[totalBalanceInBaseCurrency]="totalBalanceInBaseCurrency" [totalBalanceInBaseCurrency]="totalBalanceInBaseCurrency"
[totalValueInBaseCurrency]="totalValueInBaseCurrency" [totalValueInBaseCurrency]="totalValueInBaseCurrency"
[transactionCount]="transactionCount" [transactionCount]="transactionCount"
@ -21,7 +25,11 @@
</div> </div>
<div <div
*ngIf="!hasImpersonationId && hasPermissionToCreateAccount && !user.settings.isRestrictedView" *ngIf="
!hasImpersonationId &&
hasPermissionToCreateAccount &&
!user.settings.isRestrictedView
"
class="fab-container" class="fab-container"
> >
<a <a

48
apps/client/src/app/pages/accounts/create-or-update-account-dialog/create-or-update-account-dialog.html

@ -5,9 +5,9 @@
(ngSubmit)="onSubmit()" (ngSubmit)="onSubmit()"
> >
@if (data.account.id) { @if (data.account.id) {
<h1 i18n mat-dialog-title>Update account</h1> <h1 i18n mat-dialog-title>Update account</h1>
} @else { } @else {
<h1 i18n mat-dialog-title>Add account</h1> <h1 i18n mat-dialog-title>Add account</h1>
} }
<div class="flex-grow-1 py-3" mat-dialog-content> <div class="flex-grow-1 py-3" mat-dialog-content>
<div> <div>
@ -38,9 +38,9 @@
type="number" type="number"
(keydown.enter)="$event.stopPropagation()" (keydown.enter)="$event.stopPropagation()"
/> />
<span class="ml-2" matTextSuffix <span class="ml-2" matTextSuffix>{{
>{{ accountForm.controls['currency']?.value?.value }}</span accountForm.controls['currency']?.value?.value
> }}</span>
</mat-form-field> </mat-form-field>
</div> </div>
<div [ngClass]="{ 'd-none': platforms?.length < 1 }"> <div [ngClass]="{ 'd-none': platforms?.length < 1 }">
@ -55,18 +55,20 @@
(keydown.enter)="$event.stopPropagation()" (keydown.enter)="$event.stopPropagation()"
/> />
<mat-autocomplete #auto="matAutocomplete" [displayWith]="displayFn"> <mat-autocomplete #auto="matAutocomplete" [displayWith]="displayFn">
@for (platformEntry of filteredPlatforms | async; track platformEntry) @for (
{ platformEntry of filteredPlatforms | async;
<mat-option [value]="platformEntry"> track platformEntry
<span class="d-flex"> ) {
<gf-symbol-icon <mat-option [value]="platformEntry">
class="mr-1" <span class="d-flex">
[tooltip]="platformEntry.name" <gf-symbol-icon
[url]="platformEntry.url" class="mr-1"
/> [tooltip]="platformEntry.name"
<span>{{ platformEntry.name }}</span> [url]="platformEntry.url"
</span> />
</mat-option> <span>{{ platformEntry.name }}</span>
</span>
</mat-option>
} }
</mat-autocomplete> </mat-autocomplete>
</mat-form-field> </mat-form-field>
@ -89,12 +91,12 @@
> >
</div> </div>
@if (data.account.id) { @if (data.account.id) {
<div> <div>
<mat-form-field appearance="outline" class="w-100"> <mat-form-field appearance="outline" class="w-100">
<mat-label i18n>Account ID</mat-label> <mat-label i18n>Account ID</mat-label>
<input formControlName="accountId" matInput /> <input formControlName="accountId" matInput />
</mat-form-field> </mat-form-field>
</div> </div>
} }
</div> </div>
<div class="justify-content-end" mat-dialog-actions> <div class="justify-content-end" mat-dialog-actions>

2
apps/client/src/app/pages/admin/admin-page.html

@ -21,7 +21,7 @@
> >
<ion-icon <ion-icon
[name]="tab.iconName" [name]="tab.iconName"
[size]="deviceType === 'mobile' ? 'large': 'small'" [size]="deviceType === 'mobile' ? 'large' : 'small'"
/> />
<div class="d-none d-sm-block ml-2">{{ tab.label }}</div> <div class="d-none d-sm-block ml-2">{{ tab.label }}</div>
</a> </a>

92
apps/client/src/app/pages/blog/blog-page.html

@ -9,30 +9,30 @@
> >
</h1> </h1>
@if (hasPermissionForSubscription) { @if (hasPermissionForSubscription) {
<mat-card appearance="outlined" class="mb-3"> <mat-card appearance="outlined" class="mb-3">
<mat-card-content> <mat-card-content>
<div class="container p-0"> <div class="container p-0">
<div class="flex-nowrap no-gutters row"> <div class="flex-nowrap no-gutters row">
<a <a
class="d-flex overflow-hidden w-100" class="d-flex overflow-hidden w-100"
href="../en/blog/2023/11/black-week-2023" href="../en/blog/2023/11/black-week-2023"
> >
<div class="flex-grow-1 overflow-hidden"> <div class="flex-grow-1 overflow-hidden">
<div class="h6 m-0 text-truncate">Black Week 2023</div> <div class="h6 m-0 text-truncate">Black Week 2023</div>
<div class="d-flex text-muted">2023-11-19</div> <div class="d-flex text-muted">2023-11-19</div>
</div> </div>
<div class="align-items-center d-flex"> <div class="align-items-center d-flex">
<ion-icon <ion-icon
class="chevron text-muted" class="chevron text-muted"
name="chevron-forward-outline" name="chevron-forward-outline"
size="small" size="small"
/> />
</div> </div>
</a> </a>
</div>
</div> </div>
</div> </mat-card-content>
</mat-card-content> </mat-card>
</mat-card>
} }
<mat-card appearance="outlined" class="mb-3"> <mat-card appearance="outlined" class="mb-3">
<mat-card-content> <mat-card-content>
@ -294,30 +294,30 @@
</mat-card-content> </mat-card-content>
</mat-card> </mat-card>
@if (hasPermissionForSubscription) { @if (hasPermissionForSubscription) {
<mat-card appearance="outlined" class="mb-3"> <mat-card appearance="outlined" class="mb-3">
<mat-card-content> <mat-card-content>
<div class="container p-0"> <div class="container p-0">
<div class="flex-nowrap no-gutters row"> <div class="flex-nowrap no-gutters row">
<a <a
class="d-flex overflow-hidden w-100" class="d-flex overflow-hidden w-100"
href="../en/blog/2022/11/black-friday-2022" href="../en/blog/2022/11/black-friday-2022"
> >
<div class="flex-grow-1 overflow-hidden"> <div class="flex-grow-1 overflow-hidden">
<div class="h6 m-0 text-truncate">Black Friday 2022</div> <div class="h6 m-0 text-truncate">Black Friday 2022</div>
<div class="d-flex text-muted">2022-11-13</div> <div class="d-flex text-muted">2022-11-13</div>
</div> </div>
<div class="align-items-center d-flex"> <div class="align-items-center d-flex">
<ion-icon <ion-icon
class="chevron text-muted" class="chevron text-muted"
name="chevron-forward-outline" name="chevron-forward-outline"
size="small" size="small"
/> />
</div> </div>
</a> </a>
</div>
</div> </div>
</div> </mat-card-content>
</mat-card-content> </mat-card>
</mat-card>
} }
<mat-card appearance="outlined" class="mb-3"> <mat-card appearance="outlined" class="mb-3">
<mat-card-content> <mat-card-content>

2
apps/client/src/app/pages/faq/faq-page.html

@ -21,7 +21,7 @@
> >
<ion-icon <ion-icon
[name]="tab.iconName" [name]="tab.iconName"
[size]="deviceType === 'mobile' ? 'large': 'small'" [size]="deviceType === 'mobile' ? 'large' : 'small'"
/> />
<div class="d-none d-sm-block ml-2">{{ tab.label }}</div> <div class="d-none d-sm-block ml-2">{{ tab.label }}</div>
</a> </a>

58
apps/client/src/app/pages/features/features-page.html

@ -140,7 +140,7 @@
<h4 class="align-items-center d-flex"> <h4 class="align-items-center d-flex">
<span i18n>Portfolio Calculations</span> <span i18n>Portfolio Calculations</span>
@if (hasPermissionForSubscription) { @if (hasPermissionForSubscription) {
<gf-premium-indicator class="ml-1" /> <gf-premium-indicator class="ml-1" />
} }
</h4> </h4>
<p class="m-0"> <p class="m-0">
@ -159,7 +159,7 @@
<h4 class="align-items-center d-flex"> <h4 class="align-items-center d-flex">
<span i18n>Portfolio Allocations</span> <span i18n>Portfolio Allocations</span>
@if (hasPermissionForSubscription) { @if (hasPermissionForSubscription) {
<gf-premium-indicator class="ml-1" /> <gf-premium-indicator class="ml-1" />
} }
</h4> </h4>
<p class="m-0"> <p class="m-0">
@ -197,24 +197,24 @@
</mat-card> </mat-card>
</div> </div>
@if (hasPermissionForSubscription) { @if (hasPermissionForSubscription) {
<div class="col-xs-12 col-md-4 mb-3"> <div class="col-xs-12 col-md-4 mb-3">
<mat-card appearance="outlined" class="d-flex flex-column h-100"> <mat-card appearance="outlined" class="d-flex flex-column h-100">
<mat-card-content> <mat-card-content>
<div class="flex-grow-1"> <div class="flex-grow-1">
<h4 class="align-items-center d-flex"> <h4 class="align-items-center d-flex">
<span i18n>Market Mood</span> <span i18n>Market Mood</span>
<gf-premium-indicator class="ml-1" /> <gf-premium-indicator class="ml-1" />
</h4> </h4>
<p class="m-0"> <p class="m-0">
Check the current market mood (<a Check the current market mood (<a
[routerLink]="routerLinkResources" [routerLink]="routerLinkResources"
>Fear & Greed Index</a >Fear & Greed Index</a
>) within the app. >) within the app.
</p> </p>
</div> </div>
</mat-card-content> </mat-card-content>
</mat-card> </mat-card>
</div> </div>
} }
<div class="col-xs-12 col-md-4 mb-3"> <div class="col-xs-12 col-md-4 mb-3">
<mat-card appearance="outlined" class="d-flex flex-column h-100"> <mat-card appearance="outlined" class="d-flex flex-column h-100">
@ -223,7 +223,7 @@
<h4 class="align-items-center d-flex"> <h4 class="align-items-center d-flex">
<span i18n>Static Analysis</span> <span i18n>Static Analysis</span>
@if (hasPermissionForSubscription) { @if (hasPermissionForSubscription) {
<gf-premium-indicator class="ml-1" /> <gf-premium-indicator class="ml-1" />
} }
</h4> </h4>
<p class="m-0"> <p class="m-0">
@ -290,12 +290,16 @@
</div> </div>
</div> </div>
@if (!user) { @if (!user) {
<div class="row"> <div class="row">
<div class="col mt-3 text-center"> <div class="col mt-3 text-center">
<a color="primary" i18n mat-flat-button [routerLink]="routerLinkRegister" <a
>Get Started</a color="primary"
> i18n
mat-flat-button
[routerLink]="routerLinkRegister"
>Get Started</a
>
</div>
</div> </div>
</div>
} }
</div> </div>

2
apps/client/src/app/pages/home/home-page.html

@ -21,7 +21,7 @@
> >
<ion-icon <ion-icon
[name]="tab.iconName" [name]="tab.iconName"
[size]="deviceType === 'mobile' ? 'large': 'small'" [size]="deviceType === 'mobile' ? 'large' : 'small'"
/> />
<div class="d-none d-sm-block ml-2">{{ tab.label }}</div> <div class="d-none d-sm-block ml-2">{{ tab.label }}</div>
</a> </a>

3
apps/client/src/app/pages/landing/landing-page.html

@ -339,7 +339,8 @@
[href]="testimonial.url" [href]="testimonial.url"
>{{ testimonial.author }}</a >{{ testimonial.author }}</a
> >
<span *ngIf="!testimonial.url">{{ testimonial.author }}</span>, <span *ngIf="!testimonial.url">{{ testimonial.author }}</span
>,
{{ testimonial.country }} {{ testimonial.country }}
</div> </div>
</div> </div>

35
apps/client/src/app/pages/portfolio/activities/activities-page.html

@ -11,7 +11,11 @@
[locale]="user?.settings?.locale" [locale]="user?.settings?.locale"
[pageIndex]="pageIndex" [pageIndex]="pageIndex"
[pageSize]="pageSize" [pageSize]="pageSize"
[showActions]="!hasImpersonationId && hasPermissionToDeleteActivity && !user.settings.isRestrictedView" [showActions]="
!hasImpersonationId &&
hasPermissionToDeleteActivity &&
!user.settings.isRestrictedView
"
[sortColumn]="sortColumn" [sortColumn]="sortColumn"
[sortDirection]="sortDirection" [sortDirection]="sortDirection"
[totalItems]="totalItems" [totalItems]="totalItems"
@ -29,18 +33,21 @@
</div> </div>
</div> </div>
@if (!hasImpersonationId && hasPermissionToCreateActivity && @if (
!user.settings.isRestrictedView) { !hasImpersonationId &&
<div class="fab-container"> hasPermissionToCreateActivity &&
<a !user.settings.isRestrictedView
class="align-items-center d-flex justify-content-center" ) {
color="primary" <div class="fab-container">
mat-fab <a
[queryParams]="{ createDialog: true }" class="align-items-center d-flex justify-content-center"
[routerLink]="[]" color="primary"
> mat-fab
<ion-icon name="add-outline" size="large" /> [queryParams]="{ createDialog: true }"
</a> [routerLink]="[]"
</div> >
<ion-icon name="add-outline" size="large" />
</a>
</div>
} }
</div> </div>

122
apps/client/src/app/pages/portfolio/activities/create-or-update-activity-dialog/create-or-update-activity-dialog.html

@ -11,48 +11,61 @@
<mat-form-field appearance="outline" class="w-100"> <mat-form-field appearance="outline" class="w-100">
<mat-label i18n>Type</mat-label> <mat-label i18n>Type</mat-label>
<mat-select formControlName="type"> <mat-select formControlName="type">
<mat-select-trigger <mat-select-trigger>{{
>{{ typesTranslationMap[activityForm.controls['type'].value] typesTranslationMap[activityForm.controls['type'].value]
}}</mat-select-trigger }}</mat-select-trigger>
>
<mat-option value="BUY"> <mat-option value="BUY">
<span><b>{{ typesTranslationMap['BUY'] }}</b></span> <span
><b>{{ typesTranslationMap['BUY'] }}</b></span
>
<small class="d-block line-height-1 text-muted text-nowrap" i18n <small class="d-block line-height-1 text-muted text-nowrap" i18n
>Stocks, ETFs, bonds, cryptocurrencies, commodities</small >Stocks, ETFs, bonds, cryptocurrencies, commodities</small
> >
</mat-option> </mat-option>
<mat-option value="FEE"> <mat-option value="FEE">
<span><b>{{ typesTranslationMap['FEE'] }}</b></span> <span
><b>{{ typesTranslationMap['FEE'] }}</b></span
>
<small class="d-block line-height-1 text-muted text-nowrap" i18n <small class="d-block line-height-1 text-muted text-nowrap" i18n
>One-time fee, annual account fees</small >One-time fee, annual account fees</small
> >
</mat-option> </mat-option>
<mat-option value="DIVIDEND"> <mat-option value="DIVIDEND">
<span><b>{{ typesTranslationMap['DIVIDEND'] }}</b></span> <span
><b>{{ typesTranslationMap['DIVIDEND'] }}</b></span
>
<small class="d-block line-height-1 text-muted text-nowrap" i18n <small class="d-block line-height-1 text-muted text-nowrap" i18n
>Distribution of corporate earnings</small >Distribution of corporate earnings</small
> >
</mat-option> </mat-option>
<mat-option value="INTEREST"> <mat-option value="INTEREST">
<span><b>{{ typesTranslationMap['INTEREST'] }}</b></span> <span
><b>{{ typesTranslationMap['INTEREST'] }}</b></span
>
<small class="d-block line-height-1 text-muted text-nowrap" i18n <small class="d-block line-height-1 text-muted text-nowrap" i18n
>Revenue for lending out money</small >Revenue for lending out money</small
> >
</mat-option> </mat-option>
<mat-option value="LIABILITY"> <mat-option value="LIABILITY">
<span><b>{{ typesTranslationMap['LIABILITY'] }}</b></span> <span
><b>{{ typesTranslationMap['LIABILITY'] }}</b></span
>
<small class="d-block line-height-1 text-muted text-nowrap" i18n <small class="d-block line-height-1 text-muted text-nowrap" i18n
>Mortgages, personal loans, credit cards</small >Mortgages, personal loans, credit cards</small
> >
</mat-option> </mat-option>
<mat-option value="SELL"> <mat-option value="SELL">
<span><b>{{ typesTranslationMap['SELL'] }}</b></span> <span
><b>{{ typesTranslationMap['SELL'] }}</b></span
>
<small class="d-block line-height-1 text-muted text-nowrap" i18n <small class="d-block line-height-1 text-muted text-nowrap" i18n
>Stocks, ETFs, bonds, cryptocurrencies, commodities</small >Stocks, ETFs, bonds, cryptocurrencies, commodities</small
> >
</mat-option> </mat-option>
<mat-option value="ITEM"> <mat-option value="ITEM">
<span><b>{{ typesTranslationMap['ITEM'] }}</b></span> <span
><b>{{ typesTranslationMap['ITEM'] }}</b></span
>
<small class="d-block line-height-1 text-muted text-nowrap" i18n <small class="d-block line-height-1 text-muted text-nowrap" i18n
>Luxury items, real estate, private companies</small >Luxury items, real estate, private companies</small
> >
@ -60,16 +73,20 @@
</mat-select> </mat-select>
</mat-form-field> </mat-form-field>
</div> </div>
<div [ngClass]="{'mb-3': data.activity.id}"> <div [ngClass]="{ 'mb-3': data.activity.id }">
<mat-form-field <mat-form-field
appearance="outline" appearance="outline"
class="w-100" class="w-100"
[ngClass]="{'mb-1 without-hint': !data.activity.id}" [ngClass]="{ 'mb-1 without-hint': !data.activity.id }"
> >
<mat-label i18n>Account</mat-label> <mat-label i18n>Account</mat-label>
<mat-select formControlName="accountId"> <mat-select formControlName="accountId">
<mat-option <mat-option
*ngIf="!activityForm.controls['accountId'].hasValidator(Validators.required)" *ngIf="
!activityForm.controls['accountId'].hasValidator(
Validators.required
)
"
[value]="null" [value]="null"
/> />
<mat-option <mat-option
@ -88,14 +105,18 @@
</mat-select> </mat-select>
</mat-form-field> </mat-form-field>
</div> </div>
<div class="mb-3" [ngClass]="{'d-none': data.activity.id}"> <div class="mb-3" [ngClass]="{ 'd-none': data.activity.id }">
<mat-checkbox color="primary" formControlName="updateAccountBalance" i18n <mat-checkbox color="primary" formControlName="updateAccountBalance" i18n
>Update Cash Balance</mat-checkbox >Update Cash Balance</mat-checkbox
> >
</div> </div>
<div <div
class="mb-3" class="mb-3"
[ngClass]="{ 'd-none': !activityForm.controls['searchSymbol'].hasValidator(Validators.required) }" [ngClass]="{
'd-none': !activityForm.controls['searchSymbol'].hasValidator(
Validators.required
)
}"
> >
<mat-form-field appearance="outline" class="w-100"> <mat-form-field appearance="outline" class="w-100">
<mat-label i18n>Name, symbol or ISIN</mat-label> <mat-label i18n>Name, symbol or ISIN</mat-label>
@ -107,7 +128,11 @@
</div> </div>
<div <div
class="mb-3" class="mb-3"
[ngClass]="{ 'd-none': !activityForm.controls['name'].hasValidator(Validators.required) }" [ngClass]="{
'd-none': !activityForm.controls['name'].hasValidator(
Validators.required
)
}"
> >
<mat-form-field appearance="outline" class="w-100"> <mat-form-field appearance="outline" class="w-100">
<mat-label i18n>Name</mat-label> <mat-label i18n>Name</mat-label>
@ -118,9 +143,9 @@
<mat-form-field appearance="outline" class="w-100"> <mat-form-field appearance="outline" class="w-100">
<mat-label i18n>Currency</mat-label> <mat-label i18n>Currency</mat-label>
<mat-select formControlName="currency"> <mat-select formControlName="currency">
<mat-option *ngFor="let currency of currencies" [value]="currency" <mat-option *ngFor="let currency of currencies" [value]="currency">{{
>{{ currency }}</mat-option currency
> }}</mat-option>
</mat-select> </mat-select>
</mat-form-field> </mat-form-field>
</div> </div>
@ -146,7 +171,13 @@
</div> </div>
<div <div
class="mb-3" class="mb-3"
[ngClass]="{ 'd-none': activityForm.controls['type']?.value === 'FEE' || activityForm.controls['type']?.value === 'INTEREST' || activityForm.controls['type']?.value === 'ITEM' || activityForm.controls['type']?.value === 'LIABILITY' }" [ngClass]="{
'd-none':
activityForm.controls['type']?.value === 'FEE' ||
activityForm.controls['type']?.value === 'INTEREST' ||
activityForm.controls['type']?.value === 'ITEM' ||
activityForm.controls['type']?.value === 'LIABILITY'
}"
> >
<mat-form-field appearance="outline" class="w-100"> <mat-form-field appearance="outline" class="w-100">
<mat-label i18n>Quantity</mat-label> <mat-label i18n>Quantity</mat-label>
@ -155,7 +186,7 @@
</div> </div>
<div <div
class="mb-3" class="mb-3"
[ngClass]="{ 'd-none': activityForm.controls['type']?.value === 'FEE' }" [ngClass]="{ 'd-none': activityForm.controls['type']?.value === 'FEE' }"
> >
<div class="align-items-start d-flex"> <div class="align-items-start d-flex">
<mat-form-field appearance="outline" class="w-100"> <mat-form-field appearance="outline" class="w-100">
@ -192,17 +223,25 @@
</mat-select> </mat-select>
</div> </div>
<mat-error <mat-error
*ngIf="activityForm.controls['unitPriceInCustomCurrency'].hasError('invalid')" *ngIf="
activityForm.controls['unitPriceInCustomCurrency'].hasError(
'invalid'
)
"
><ng-container i18n ><ng-container i18n
>Oops! Could not get the historical exchange rate >Oops! Could not get the historical exchange rate
from</ng-container from</ng-container
> >
{{ activityForm.controls['date']?.value | date: defaultDateFormat {{
activityForm.controls['date']?.value | date: defaultDateFormat
}}</mat-error }}</mat-error
> >
</mat-form-field> </mat-form-field>
<button <button
*ngIf="currentMarketPrice && (data.activity.type === 'BUY' || data.activity.type === 'SELL')" *ngIf="
currentMarketPrice &&
(data.activity.type === 'BUY' || data.activity.type === 'SELL')
"
class="ml-2 mt-1 no-min-width" class="ml-2 mt-1 no-min-width"
mat-button mat-button
title="Apply current market price" title="Apply current market price"
@ -228,14 +267,19 @@
</ng-container> </ng-container>
</mat-label> </mat-label>
<input formControlName="unitPrice" matInput type="number" /> <input formControlName="unitPrice" matInput type="number" />
<span class="ml-2" matTextSuffix <span class="ml-2" matTextSuffix>{{
>{{ activityForm.controls['currency'].value }}</span activityForm.controls['currency'].value
> }}</span>
</mat-form-field> </mat-form-field>
</div> </div>
<div <div
class="mb-3" class="mb-3"
[ngClass]="{ 'd-none': activityForm.controls['type']?.value === 'INTEREST' || activityForm.controls['type']?.value === 'ITEM' || activityForm.controls['type']?.value === 'LIABILITY' }" [ngClass]="{
'd-none':
activityForm.controls['type']?.value === 'INTEREST' ||
activityForm.controls['type']?.value === 'ITEM' ||
activityForm.controls['type']?.value === 'LIABILITY'
}"
> >
<mat-form-field appearance="outline" class="w-100"> <mat-form-field appearance="outline" class="w-100">
<mat-label i18n>Fee</mat-label> <mat-label i18n>Fee</mat-label>
@ -252,11 +296,14 @@
</mat-select> </mat-select>
</div> </div>
<mat-error <mat-error
*ngIf="activityForm.controls['feeInCustomCurrency'].hasError('invalid')" *ngIf="
activityForm.controls['feeInCustomCurrency'].hasError('invalid')
"
><ng-container i18n ><ng-container i18n
>Oops! Could not get the historical exchange rate from</ng-container >Oops! Could not get the historical exchange rate from</ng-container
> >
{{ activityForm.controls['date']?.value | date: defaultDateFormat {{
activityForm.controls['date']?.value | date: defaultDateFormat
}}</mat-error }}</mat-error
> >
</mat-form-field> </mat-form-field>
@ -265,9 +312,9 @@
<mat-form-field appearance="outline" class="w-100"> <mat-form-field appearance="outline" class="w-100">
<mat-label i18n>Fee</mat-label> <mat-label i18n>Fee</mat-label>
<input formControlName="fee" matInput type="number" /> <input formControlName="fee" matInput type="number" />
<span class="ml-2" matTextSuffix <span class="ml-2" matTextSuffix>{{
>{{ activityForm.controls['currency'].value }}</span activityForm.controls['currency'].value
> }}</span>
</mat-form-field> </mat-form-field>
</div> </div>
<div class="mb-3"> <div class="mb-3">
@ -340,7 +387,7 @@
(optionSelected)="onAddTag($event)" (optionSelected)="onAddTag($event)"
> >
<mat-option <mat-option
*ngFor="let tag of filteredTagsObservable | async" *ngFor="let tag of filteredTagsObservable | async"
[value]="tag.id" [value]="tag.id"
> >
{{ tag.name }} {{ tag.name }}
@ -354,7 +401,10 @@
class="flex-grow-1" class="flex-grow-1"
[isCurrency]="true" [isCurrency]="true"
[locale]="data.user?.settings?.locale" [locale]="data.user?.settings?.locale"
[unit]="activityForm.controls['currency']?.value ?? data.user?.settings?.baseCurrency" [unit]="
activityForm.controls['currency']?.value ??
data.user?.settings?.baseCurrency
"
[value]="total" [value]="total"
/> />
<div> <div>

279
apps/client/src/app/pages/portfolio/activities/import-activities-dialog/import-activities-dialog.html

@ -25,81 +25,86 @@
</ng-template> </ng-template>
<div class="pt-3"> <div class="pt-3">
@if (mode === 'DIVIDEND') { @if (mode === 'DIVIDEND') {
<form <form
[formGroup]="uniqueAssetForm" [formGroup]="uniqueAssetForm"
(ngSubmit)="onLoadDividends(stepper)" (ngSubmit)="onLoadDividends(stepper)"
> >
<mat-form-field appearance="outline" class="w-100"> <mat-form-field appearance="outline" class="w-100">
<mat-label i18n>Holding</mat-label> <mat-label i18n>Holding</mat-label>
<mat-select formControlName="uniqueAsset"> <mat-select formControlName="uniqueAsset">
<mat-select-trigger <mat-select-trigger>{{
>{{ uniqueAssetForm.controls['uniqueAsset']?.value?.name uniqueAssetForm.controls['uniqueAsset']?.value?.name
}}</mat-select-trigger }}</mat-select-trigger>
> <mat-option
<mat-option *ngFor="let holding of holdings"
*ngFor="let holding of holdings" class="line-height-1"
class="line-height-1" [value]="{
[value]="{ dataSource: holding.dataSource, name: holding.name, symbol: holding.symbol }" dataSource: holding.dataSource,
> name: holding.name,
<span><b>{{ holding.name }}</b></span> symbol: holding.symbol
<br /> }"
<small class="text-muted"
>{{ holding.symbol | gfSymbol }} · {{ holding.currency
}}</small
> >
</mat-option> <span
</mat-select> ><b>{{ holding.name }}</b></span
<mat-spinner >
*ngIf="isLoading" <br />
class="position-absolute" <small class="text-muted"
[diameter]="20" >{{ holding.symbol | gfSymbol }} ·
/> {{ holding.currency }}</small
</mat-form-field> >
</mat-option>
</mat-select>
<mat-spinner
*ngIf="isLoading"
class="position-absolute"
[diameter]="20"
/>
</mat-form-field>
<div class="d-flex flex-column justify-content-center">
<button
color="primary"
mat-flat-button
type="submit"
[disabled]="!uniqueAssetForm.valid"
>
<span i18n>Load Dividends</span>
</button>
</div>
</form>
} @else {
<div class="d-flex flex-column justify-content-center"> <div class="d-flex flex-column justify-content-center">
<button <button
color="primary" class="drop-area p-4 text-center text-muted"
mat-flat-button gfFileDrop
type="submit" (click)="onSelectFile(stepper)"
[disabled]="!uniqueAssetForm.valid" (filesDropped)="onFilesDropped({ stepper, files: $event })"
> >
<span i18n>Load Dividends</span> <div
class="align-items-center d-flex flex-column justify-content-center"
>
<ion-icon class="cloud-icon" name="cloud-upload-outline" />
<span i18n>Choose or drop a file here</span>
</div>
</button> </button>
<p class="mb-0 mt-3 text-center">
<small>
<span class="mr-1" i18n
>The following file formats are supported:</span
>
<a
href="https://github.com/ghostfolio/ghostfolio/blob/main/test/import/ok.csv"
target="_blank"
>CSV</a
>
<span class="mx-1" i18n>or</span>
<a
href="https://github.com/ghostfolio/ghostfolio/blob/main/test/import/ok.json"
target="_blank"
>JSON</a
>
</small>
</p>
</div> </div>
</form>
} @else {
<div class="d-flex flex-column justify-content-center">
<button
class="drop-area p-4 text-center text-muted"
gfFileDrop
(click)="onSelectFile(stepper)"
(filesDropped)="onFilesDropped({stepper, files: $event})"
>
<div
class="align-items-center d-flex flex-column justify-content-center"
>
<ion-icon class="cloud-icon" name="cloud-upload-outline" />
<span i18n>Choose or drop a file here</span>
</div>
</button>
<p class="mb-0 mt-3 text-center">
<small>
<span class="mr-1" i18n
>The following file formats are supported:</span
>
<a
href="https://github.com/ghostfolio/ghostfolio/blob/main/test/import/ok.csv"
target="_blank"
>CSV</a
>
<span class="mx-1" i18n>or</span>
<a
href="https://github.com/ghostfolio/ghostfolio/blob/main/test/import/ok.json"
target="_blank"
>JSON</a
>
</small>
</p>
</div>
} }
</div> </div>
</mat-step> </mat-step>
@ -114,76 +119,76 @@
> >
</ng-template> </ng-template>
<div class="pt-3"> <div class="pt-3">
@if(errorMessages?.length === 0) { @if (errorMessages?.length === 0) {
<gf-activities-table <gf-activities-table
*ngIf="importStep === 1" *ngIf="importStep === 1"
[baseCurrency]="data?.user?.settings?.baseCurrency" [baseCurrency]="data?.user?.settings?.baseCurrency"
[dataSource]="dataSource" [dataSource]="dataSource"
[deviceType]="data?.deviceType" [deviceType]="data?.deviceType"
[hasPermissionToCreateActivity]="false" [hasPermissionToCreateActivity]="false"
[hasPermissionToExportActivities]="false" [hasPermissionToExportActivities]="false"
[hasPermissionToFilter]="false" [hasPermissionToFilter]="false"
[hasPermissionToOpenDetails]="false" [hasPermissionToOpenDetails]="false"
[locale]="data?.user?.settings?.locale" [locale]="data?.user?.settings?.locale"
[pageSize]="maxSafeInteger" [pageSize]="maxSafeInteger"
[showActions]="false" [showActions]="false"
[showCheckbox]="true" [showCheckbox]="true"
[showSymbolColumn]="false" [showSymbolColumn]="false"
[sortColumn]="sortColumn" [sortColumn]="sortColumn"
[sortDirection]="sortDirection" [sortDirection]="sortDirection"
[sortDisabled]="true" [sortDisabled]="true"
[totalItems]="totalItems" [totalItems]="totalItems"
(selectedActivities)="updateSelection($event)" (selectedActivities)="updateSelection($event)"
/> />
<div class="d-flex justify-content-end mt-3"> <div class="d-flex justify-content-end mt-3">
<button mat-button (click)="onReset(stepper)"> <button mat-button (click)="onReset(stepper)">
<ng-container i18n>Back</ng-container> <ng-container i18n>Back</ng-container>
</button> </button>
<button <button
class="ml-1" class="ml-1"
color="primary" color="primary"
mat-flat-button mat-flat-button
[disabled]="!selectedActivities?.length" [disabled]="!selectedActivities?.length"
(click)="onImportActivities()" (click)="onImportActivities()"
> >
<ng-container i18n>Import</ng-container> <ng-container i18n>Import</ng-container>
</button> </button>
</div> </div>
} @else { } @else {
<mat-accordion displayMode="flat"> <mat-accordion displayMode="flat">
<mat-expansion-panel <mat-expansion-panel
*ngFor="let message of errorMessages; let i = index" *ngFor="let message of errorMessages; let i = index"
[disabled]="!details[i]" [disabled]="!details[i]"
> >
<mat-expansion-panel-header class="pl-1"> <mat-expansion-panel-header class="pl-1">
<mat-panel-title> <mat-panel-title>
<div class="d-flex"> <div class="d-flex">
<div class="align-items-center d-flex mr-2"> <div class="align-items-center d-flex mr-2">
<ion-icon name="warning-outline" /> <ion-icon name="warning-outline" />
</div>
<div>{{ message }}</div>
</div> </div>
<div>{{ message }}</div> </mat-panel-title>
</div> </mat-expansion-panel-header>
</mat-panel-title> <pre
</mat-expansion-panel-header> *ngIf="details[i]"
<pre class="m-0"
*ngIf="details[i]" ><code>{{ details[i] | json }}</code></pre>
class="m-0" </mat-expansion-panel>
><code>{{ details[i] | json }}</code></pre> </mat-accordion>
</mat-expansion-panel> <div class="d-flex justify-content-end mt-3">
</mat-accordion> <button mat-button (click)="onReset(stepper)">
<div class="d-flex justify-content-end mt-3"> <ng-container i18n>Back</ng-container>
<button mat-button (click)="onReset(stepper)"> </button>
<ng-container i18n>Back</ng-container> <button
</button> class="ml-1"
<button color="primary"
class="ml-1" mat-flat-button
color="primary" [disabled]="true"
mat-flat-button >
[disabled]="true" <ng-container i18n>Import</ng-container>
> </button>
<ng-container i18n>Import</ng-container> </div>
</button>
</div>
} }
</div> </div>
</mat-step> </mat-step>

15
apps/client/src/app/pages/portfolio/allocations/allocations-page.html

@ -15,13 +15,20 @@
class="justify-content-end l-2" class="justify-content-end l-2"
size="medium" size="medium"
[isPercent]="true" [isPercent]="true"
[value]="isLoading ? undefined : portfolioDetails?.filteredValueInPercentage" [value]="
isLoading
? undefined
: portfolioDetails?.filteredValueInPercentage
"
/> />
</mat-card-header> </mat-card-header>
<mat-card-content> <mat-card-content>
<mat-progress-bar <mat-progress-bar
mode="determinate" mode="determinate"
[title]="(portfolioDetails?.filteredValueInPercentage * 100).toFixed(2) + '%'" [title]="
(portfolioDetails?.filteredValueInPercentage * 100).toFixed(2) +
'%'
"
[value]="portfolioDetails?.filteredValueInPercentage * 100" [value]="portfolioDetails?.filteredValueInPercentage * 100"
/> />
</mat-card-content> </mat-card-content>
@ -204,7 +211,9 @@
<gf-world-map-chart <gf-world-map-chart
[countries]="countries" [countries]="countries"
[format]="worldMapChartFormat" [format]="worldMapChartFormat"
[isInPercent]="hasImpersonationId || user.settings.isRestrictedView" [isInPercent]="
hasImpersonationId || user.settings.isRestrictedView
"
[locale]="user?.settings?.locale" [locale]="user?.settings?.locale"
/> />
</div> </div>

213
apps/client/src/app/pages/portfolio/analysis/analysis-page.html

@ -19,105 +19,134 @@
</div> </div>
@if (user?.settings?.isExperimentalFeatures) { @if (user?.settings?.isExperimentalFeatures) {
<div class="mb-5 row"> <div class="mb-5 row">
<div class="col"> <div class="col">
<mat-card appearance="outlined" class="mb-3"> <mat-card appearance="outlined" class="mb-3">
<mat-card-content> <mat-card-content>
<div class="d-flex py-1"> <div class="d-flex py-1">
<div class="flex-grow-1 mr-2 text-truncate" i18n> <div class="flex-grow-1 mr-2 text-truncate" i18n>
Absolute Asset Performance Absolute Asset Performance
</div> </div>
<div class="d-flex justify-content-end"> <div class="d-flex justify-content-end">
<gf-value <gf-value
class="justify-content-end" class="justify-content-end"
position="end" position="end"
[isCurrency]="true" [isCurrency]="true"
[locale]="user?.settings?.locale" [locale]="user?.settings?.locale"
[unit]="user?.settings?.baseCurrency" [unit]="user?.settings?.baseCurrency"
[value]="isLoadingInvestmentChart ? undefined : performance?.currentNetPerformance" [value]="
/> isLoadingInvestmentChart
</div> ? undefined
</div> : performance?.currentNetPerformance
<div class="d-flex mb-3 ml-3 py-1"> "
<div class="flex-grow-1 mr-2 text-truncate" i18n> />
Asset Performance </div>
</div> </div>
<div class="d-flex justify-content-end"> <div class="d-flex mb-3 ml-3 py-1">
<gf-value <div class="flex-grow-1 mr-2 text-truncate" i18n>
class="justify-content-end" Asset Performance
position="end" </div>
[colorizeSign]="true" <div class="d-flex justify-content-end">
[isPercent]="true" <gf-value
[locale]="user?.settings?.locale" class="justify-content-end"
[value]="isLoadingInvestmentChart ? undefined : performance?.currentNetPerformancePercent" position="end"
/> [colorizeSign]="true"
[isPercent]="true"
[locale]="user?.settings?.locale"
[value]="
isLoadingInvestmentChart
? undefined
: performance?.currentNetPerformancePercent
"
/>
</div>
</div> </div>
</div> <div class="d-flex py-1">
<div class="d-flex py-1"> <div class="flex-grow-1 mr-2 text-truncate" i18n>
<div class="flex-grow-1 mr-2 text-truncate" i18n> Absolute Currency Performance
Absolute Currency Performance </div>
<div class="d-flex justify-content-end">
<gf-value
class="justify-content-end"
position="end"
[isCurrency]="true"
[locale]="user?.settings?.locale"
[unit]="user?.settings?.baseCurrency"
[value]="
isLoadingInvestmentChart
? undefined
: performance?.currentNetPerformanceWithCurrencyEffect ===
null
? null
: performance?.currentNetPerformanceWithCurrencyEffect -
performance?.currentNetPerformance
"
/>
</div>
</div> </div>
<div class="d-flex justify-content-end"> <div class="d-flex ml-3 py-1">
<gf-value <div class="flex-grow-1 mr-2 text-truncate" i18n>
class="justify-content-end" Currency Performance
position="end" </div>
[isCurrency]="true" <div class="d-flex justify-content-end">
[locale]="user?.settings?.locale" <gf-value
[unit]="user?.settings?.baseCurrency" class="justify-content-end"
[value]="isLoadingInvestmentChart ? undefined : (performance?.currentNetPerformanceWithCurrencyEffect === null ? null : performance?.currentNetPerformanceWithCurrencyEffect - performance?.currentNetPerformance)" position="end"
/> [colorizeSign]="true"
[isPercent]="true"
[locale]="user?.settings?.locale"
[value]="
isLoadingInvestmentChart
? undefined
: performance?.currentNetPerformancePercentWithCurrencyEffect -
performance?.currentNetPerformancePercent
"
/>
</div>
</div> </div>
</div> <div><hr /></div>
<div class="d-flex ml-3 py-1"> <div class="d-flex py-1">
<div class="flex-grow-1 mr-2 text-truncate" i18n> <div class="flex-grow-1 mr-2 text-truncate" i18n>
Currency Performance Absolute Net Performance
</div>
<div class="d-flex justify-content-end">
<gf-value
class="justify-content-end"
position="end"
[isCurrency]="true"
[locale]="user?.settings?.locale"
[unit]="user?.settings?.baseCurrency"
[value]="
isLoadingInvestmentChart
? undefined
: performance?.currentNetPerformanceWithCurrencyEffect
"
/>
</div>
</div> </div>
<div class="d-flex justify-content-end"> <div class="d-flex ml-3 py-1">
<gf-value <div class="flex-grow-1 mr-2 text-truncate" i18n>
class="justify-content-end" Net Performance
position="end" </div>
[colorizeSign]="true" <div class="d-flex justify-content-end">
[isPercent]="true" <gf-value
[locale]="user?.settings?.locale" class="justify-content-end"
[value]="isLoadingInvestmentChart ? undefined : performance?.currentNetPerformancePercentWithCurrencyEffect - performance?.currentNetPerformancePercent" position="end"
/> [colorizeSign]="true"
</div> [isPercent]="true"
</div> [locale]="user?.settings?.locale"
<div><hr /></div> [value]="
<div class="d-flex py-1"> isLoadingInvestmentChart
<div class="flex-grow-1 mr-2 text-truncate" i18n> ? undefined
Absolute Net Performance : performance?.currentNetPerformancePercentWithCurrencyEffect
"
/>
</div>
</div> </div>
<div class="d-flex justify-content-end"> </mat-card-content>
<gf-value </mat-card>
class="justify-content-end" </div>
position="end"
[isCurrency]="true"
[locale]="user?.settings?.locale"
[unit]="user?.settings?.baseCurrency"
[value]="isLoadingInvestmentChart ? undefined : performance?.currentNetPerformanceWithCurrencyEffect"
/>
</div>
</div>
<div class="d-flex ml-3 py-1">
<div class="flex-grow-1 mr-2 text-truncate" i18n>
Net Performance
</div>
<div class="d-flex justify-content-end">
<gf-value
class="justify-content-end"
position="end"
[colorizeSign]="true"
[isPercent]="true"
[locale]="user?.settings?.locale"
[value]="isLoadingInvestmentChart ? undefined : performance?.currentNetPerformancePercentWithCurrencyEffect"
/>
</div>
</div>
</mat-card-content>
</mat-card>
</div> </div>
</div>
} }
<div class="mb-5 row"> <div class="mb-5 row">

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

@ -16,11 +16,14 @@
[currency]="user?.settings?.baseCurrency" [currency]="user?.settings?.baseCurrency"
[deviceType]="deviceType" [deviceType]="deviceType"
[fireWealth]="fireWealth?.toNumber()" [fireWealth]="fireWealth?.toNumber()"
[hasPermissionToUpdateUserSettings]="!hasImpersonationId && hasPermissionToUpdateUserSettings" [hasPermissionToUpdateUserSettings]="
!hasImpersonationId && hasPermissionToUpdateUserSettings
"
[locale]="user?.settings?.locale" [locale]="user?.settings?.locale"
[ngStyle]="{ [ngStyle]="{
opacity: user?.subscription?.type === 'Basic' ? '0.67' : 'initial', opacity: user?.subscription?.type === 'Basic' ? '0.67' : 'initial',
'pointer-events': user?.subscription?.type === 'Basic' ? 'none' : 'initial' 'pointer-events':
user?.subscription?.type === 'Basic' ? 'none' : 'initial'
}" }"
[projectedTotalAmount]="user?.settings?.projectedTotalAmount" [projectedTotalAmount]="user?.settings?.projectedTotalAmount"
[retirementDate]="user?.settings?.retirementDate" [retirementDate]="user?.settings?.retirementDate"

18
apps/client/src/app/pages/portfolio/holdings/holdings-page.html

@ -14,15 +14,15 @@
[locale]="user?.settings?.locale" [locale]="user?.settings?.locale"
/> />
@if (hasPermissionToCreateOrder && holdings?.length > 0) { @if (hasPermissionToCreateOrder && holdings?.length > 0) {
<div class="text-center"> <div class="text-center">
<a <a
class="mt-3" class="mt-3"
i18n i18n
mat-stroked-button mat-stroked-button
[routerLink]="['/portfolio', 'activities']" [routerLink]="['/portfolio', 'activities']"
>Manage Activities</a >Manage Activities</a
> >
</div> </div>
} }
</div> </div>
</div> </div>

2
apps/client/src/app/pages/portfolio/portfolio-page.html

@ -21,7 +21,7 @@
> >
<ion-icon <ion-icon
[name]="tab.iconName" [name]="tab.iconName"
[size]="deviceType === 'mobile' ? 'large': 'small'" [size]="deviceType === 'mobile' ? 'large' : 'small'"
/> />
<div class="d-none d-sm-block ml-2">{{ tab.label }}</div> <div class="d-none d-sm-block ml-2">{{ tab.label }}</div>
</a> </a>

26
apps/client/src/app/pages/pricing/pricing-page.html

@ -107,7 +107,7 @@
<mat-card <mat-card
appearance="outlined" appearance="outlined"
class="h-100" class="h-100"
[ngClass]="{ 'active': user?.subscription?.type === 'Basic' }" [ngClass]="{ active: user?.subscription?.type === 'Basic' }"
> >
<mat-card-content class="d-flex flex-column h-100"> <mat-card-content class="d-flex flex-column h-100">
<div class="flex-grow-1"> <div class="flex-grow-1">
@ -164,7 +164,7 @@
<mat-card <mat-card
appearance="outlined" appearance="outlined"
class="h-100" class="h-100"
[ngClass]="{ 'active': user?.subscription?.type === 'Premium' }" [ngClass]="{ active: user?.subscription?.type === 'Premium' }"
> >
<mat-card-content class="d-flex flex-column h-100"> <mat-card-content class="d-flex flex-column h-100">
<div class="flex-grow-1"> <div class="flex-grow-1">
@ -243,19 +243,22 @@
<ng-container *ngIf="coupon" <ng-container *ngIf="coupon"
><del class="text-muted" ><del class="text-muted"
>{{ baseCurrency }}&nbsp;{{ price }}</del >{{ baseCurrency }}&nbsp;{{ price }}</del
>&nbsp;{{ baseCurrency }}&nbsp;<strong >&nbsp;{{ baseCurrency }}&nbsp;<strong>{{
>{{ price - coupon }}</strong price - coupon
> }}</strong>
</ng-container> </ng-container>
<ng-container *ngIf="!coupon" <ng-container *ngIf="!coupon"
>{{ baseCurrency }}&nbsp;<strong >{{ baseCurrency }}&nbsp;<strong>{{
>{{ price }}</strong price
></ng-container }}</strong></ng-container
>&nbsp;<span i18n>per year</span></span >&nbsp;<span i18n>per year</span></span
> >
</p> </p>
<div <div
*ngIf="hasPermissionToUpdateUserSettings && user?.subscription?.type === 'Basic'" *ngIf="
hasPermissionToUpdateUserSettings &&
user?.subscription?.type === 'Basic'
"
class="mt-3 text-center" class="mt-3 text-center"
> >
<button color="primary" mat-flat-button (click)="onCheckout()"> <button color="primary" mat-flat-button (click)="onCheckout()">
@ -265,7 +268,10 @@
>Upgrade Plan</ng-container >Upgrade Plan</ng-container
> >
<ng-container <ng-container
*ngIf="user.subscription.offer === 'renewal' || user.subscription.offer === 'renewal-early-bird'" *ngIf="
user.subscription.offer === 'renewal' ||
user.subscription.offer === 'renewal-early-bird'
"
i18n i18n
>Renew Plan</ng-container >Renew Plan</ng-container
> >

6
apps/client/src/app/pages/register/show-access-token-dialog/show-access-token-dialog.html

@ -1,8 +1,8 @@
<h1 mat-dialog-title> <h1 mat-dialog-title>
<span i18n>Create Account</span <span i18n>Create Account</span
><span *ngIf="data.role === 'ADMIN'" class="badge badge-light ml-2" ><span *ngIf="data.role === 'ADMIN'" class="badge badge-light ml-2">{{
>{{ data.role }}</span data.role
> }}</span>
</h1> </h1>
<div class="py-3" mat-dialog-content> <div class="py-3" mat-dialog-content>
<div> <div>

54
apps/client/src/app/pages/resources/personal-finance-tools/personal-finance-tools-page.html

@ -19,32 +19,38 @@
</p> </p>
</div> </div>
@for (product of products; track product) { @for (product of products; track product) {
<mat-card appearance="outlined" class="mb-3"> <mat-card appearance="outlined" class="mb-3">
<mat-card-content> <mat-card-content>
<div class="container p-0"> <div class="container p-0">
<div class="flex-nowrap no-gutters row"> <div class="flex-nowrap no-gutters row">
<a <a
class="d-flex overflow-hidden w-100" class="d-flex overflow-hidden w-100"
title="Compare Ghostfolio to {{ product.name }} - {{ product.slogan }}" title="Compare Ghostfolio to {{ product.name }} - {{
[routerLink]="[pathResources, 'personal-finance-tools', pathAlternativeTo + (product.alias ?? product.key)]" product.slogan
> }}"
<div class="flex-grow-1 overflow-hidden"> [routerLink]="[
<div class="h6 m-0 text-truncate" i18n> pathResources,
Open Source Alternative to {{ product.name }} 'personal-finance-tools',
pathAlternativeTo + (product.alias ?? product.key)
]"
>
<div class="flex-grow-1 overflow-hidden">
<div class="h6 m-0 text-truncate" i18n>
Open Source Alternative to {{ product.name }}
</div>
</div> </div>
</div> <div class="align-items-center d-flex">
<div class="align-items-center d-flex"> <ion-icon
<ion-icon class="chevron text-muted"
class="chevron text-muted" name="chevron-forward-outline"
name="chevron-forward-outline" size="small"
size="small" />
/> </div>
</div> </a>
</a> </div>
</div> </div>
</div> </mat-card-content>
</mat-card-content> </mat-card>
</mat-card>
} }
</div> </div>
</div> </div>

42
apps/client/src/app/pages/resources/personal-finance-tools/product-page-template.html

@ -11,8 +11,9 @@
</div> </div>
<section class="mb-4"> <section class="mb-4">
<p i18n> <p i18n>
Are you looking for an open source alternative to {{ product2.name Are you looking for an open source alternative to
}}? <a [routerLink]="routerLinkAbout">Ghostfolio</a> is a powerful {{ product2.name }}?
<a [routerLink]="routerLinkAbout">Ghostfolio</a> is a powerful
portfolio management tool that provides individuals with a portfolio management tool that provides individuals with a
comprehensive platform to track, analyze, and optimize their comprehensive platform to track, analyze, and optimize their
investments. Whether you are an experienced investor or just investments. Whether you are an experienced investor or just
@ -35,18 +36,22 @@
its capabilities, security, and user experience. its capabilities, security, and user experience.
</p> </p>
<p i18n> <p i18n>
Let’s dive deeper into the detailed Ghostfolio vs {{ product2.name Let’s dive deeper into the detailed Ghostfolio vs
}} comparison table below to gain a thorough understanding of how {{ product2.name }} comparison table below to gain a thorough
Ghostfolio positions itself relative to {{ product2.name }}. We will understanding of how Ghostfolio positions itself relative to
explore various aspects such as features, data privacy, pricing, and {{ product2.name }}. We will explore various aspects such as
more, allowing you to make a well-informed choice for your personal features, data privacy, pricing, and more, allowing you to make a
requirements. well-informed choice for your personal requirements.
</p> </p>
</section> </section>
<section class="mb-4"> <section class="mb-4">
<table class="gf-table w-100"> <table class="gf-table w-100">
<caption class="text-center" i18n> <caption class="text-center" i18n>
Ghostfolio vs {{ product2.name }} comparison table Ghostfolio vs
{{
product2.name
}}
comparison table
</caption> </caption>
<thead> <thead>
<tr class="mat-mdc-header-row"> <tr class="mat-mdc-header-row">
@ -187,8 +192,9 @@
</td> </td>
<td class="mat-mdc-cell px-1 py-2"> <td class="mat-mdc-cell px-1 py-2">
<ng-container *ngIf="product2.pricingPerYear" <ng-container *ngIf="product2.pricingPerYear"
><span i18n>Starting from</span> {{ product2.pricingPerYear ><span i18n>Starting from</span>
}} / <span i18n>year</span></ng-container {{ product2.pricingPerYear }} /
<span i18n>year</span></ng-container
> >
</td> </td>
</tr> </tr>
@ -202,13 +208,13 @@
</section> </section>
<section class="mb-4"> <section class="mb-4">
<p i18n> <p i18n>
Please note that the information provided in the Ghostfolio vs {{ Please note that the information provided in the Ghostfolio vs
product2.name }} comparison table is based on our independent {{ product2.name }} comparison table is based on our independent
research and analysis. This website is not affiliated with {{ research and analysis. This website is not affiliated with
product2.name }} or any other product mentioned in the comparison. {{ product2.name }} or any other product mentioned in the
As the landscape of personal finance tools evolves, it is essential comparison. As the landscape of personal finance tools evolves, it
to verify any specific details or changes directly from the is essential to verify any specific details or changes directly from
respective product page. Data needs a refresh? Help us maintain the respective product page. Data needs a refresh? Help us maintain
accurate data on accurate data on
<a href="https://github.com/ghostfolio/ghostfolio">GitHub</a>. <a href="https://github.com/ghostfolio/ghostfolio">GitHub</a>.
</p> </p>

26
apps/client/src/app/pages/resources/resources-page.html

@ -171,21 +171,21 @@
</div> </div>
</div> </div>
@if (hasPermissionForSubscription) { @if (hasPermissionForSubscription) {
<div class="mb-4 media"> <div class="mb-4 media">
<div class="media-body"> <div class="media-body">
<h3 class="h5 mt-0">Personal Finance Tools</h3> <h3 class="h5 mt-0">Personal Finance Tools</h3>
<div class="mb-1"> <div class="mb-1">
Personal finance tools are software applications that help Personal finance tools are software applications that help
individuals manage their money, track expenses, set budgets, individuals manage their money, track expenses, set budgets,
monitor investments, and make informed financial decisions. monitor investments, and make informed financial decisions.
</div> </div>
<div> <div>
<a [routerLink]="routerLinkResourcesPersonalFinanceTools" <a [routerLink]="routerLinkResourcesPersonalFinanceTools"
>Personal Finance Tools →</a >Personal Finance Tools →</a
> >
</div>
</div> </div>
</div> </div>
</div>
} }
<div class="mb-4 media"> <div class="mb-4 media">
<div class="media-body"> <div class="media-body">

2
apps/client/src/app/pages/user-account/user-account-page.html

@ -21,7 +21,7 @@
> >
<ion-icon <ion-icon
[name]="tab.iconName" [name]="tab.iconName"
[size]="deviceType === 'mobile' ? 'large': 'small'" [size]="deviceType === 'mobile' ? 'large' : 'small'"
/> />
<div class="d-none d-sm-block ml-2">{{ tab.label }}</div> <div class="d-none d-sm-block ml-2">{{ tab.label }}</div>
</a> </a>

42
apps/client/src/app/pages/webauthn/webauthn-page.html

@ -8,29 +8,29 @@
</div> </div>
@if (!hasError) { @if (!hasError) {
<div class="col d-flex justify-content-center"> <div class="col d-flex justify-content-center">
<mat-spinner [diameter]="20" /> <mat-spinner [diameter]="20" />
</div> </div>
} @else { } @else {
<div <div
class="align-items-center col d-flex flex-column justify-content-center" class="align-items-center col d-flex flex-column justify-content-center"
>
<h1 class="d-flex h5 justify-content-center mb-0 text-center">
<ng-container i18n>Oops, authentication has failed.</ng-container>
</h1>
<button
class="mb-3 mt-4"
color="primary"
mat-flat-button
(click)="signIn()"
> >
<ng-container i18n>Try again</ng-container> <h1 class="d-flex h5 justify-content-center mb-0 text-center">
</button> <ng-container i18n>Oops, authentication has failed.</ng-container>
<div class="text-muted"><ng-container i18n>or</ng-container></div> </h1>
<button class="mt-1" mat-flat-button (click)="deregisterDevice()"> <button
<ng-container i18n>Go back to Home Page</ng-container> class="mb-3 mt-4"
</button> color="primary"
</div> mat-flat-button
(click)="signIn()"
>
<ng-container i18n>Try again</ng-container>
</button>
<div class="text-muted"><ng-container i18n>or</ng-container></div>
<button class="mt-1" mat-flat-button (click)="deregisterDevice()">
<ng-container i18n>Go back to Home Page</ng-container>
</button>
</div>
} }
</div> </div>
</div> </div>

2
apps/client/src/app/pages/zen/zen-page.html

@ -21,7 +21,7 @@
> >
<ion-icon <ion-icon
[name]="tab.iconName" [name]="tab.iconName"
[size]="deviceType === 'mobile' ? 'large': 'small'" [size]="deviceType === 'mobile' ? 'large' : 'small'"
/> />
<div class="d-none d-sm-block ml-2">{{ tab.label }}</div> <div class="d-none d-sm-block ml-2">{{ tab.label }}</div>
</a> </a>

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

@ -6,6 +6,7 @@ import {
DATE_FORMAT_MONTHLY, DATE_FORMAT_MONTHLY,
DATE_FORMAT_YEARLY, DATE_FORMAT_YEARLY,
getBackgroundColor, getBackgroundColor,
getLocale,
getTextColor getTextColor
} from './helper'; } from './helper';
import { ColorScheme, GroupBy } from './types'; import { ColorScheme, GroupBy } from './types';
@ -30,7 +31,7 @@ export function getTooltipOptions({
colorScheme, colorScheme,
currency = '', currency = '',
groupBy, groupBy,
locale = '', locale = getLocale(),
unit = '' unit = ''
}: { }: {
colorScheme?: ColorScheme; colorScheme?: ColorScheme;

6
libs/common/src/lib/helper.ts

@ -217,9 +217,7 @@ export function getEmojiFlag(aCountryCode: string) {
} }
export function getLocale() { export function getLocale() {
return navigator.languages?.length return navigator.language ?? locale;
? navigator.languages[0]
: navigator.language ?? locale;
} }
export function getNumberFormatDecimal(aLocale?: string) { export function getNumberFormatDecimal(aLocale?: string) {
@ -230,7 +228,7 @@ export function getNumberFormatDecimal(aLocale?: string) {
}).value; }).value;
} }
export function getNumberFormatGroup(aLocale?: string) { export function getNumberFormatGroup(aLocale = getLocale()) {
const formatObject = new Intl.NumberFormat(aLocale).formatToParts(9999.99); const formatObject = new Intl.NumberFormat(aLocale).formatToParts(9999.99);
return formatObject.find((object) => { return formatObject.find((object) => {

3
libs/ui/src/lib/account-balances/account-balances.component.ts

@ -1,3 +1,4 @@
import { getLocale } from '@ghostfolio/common/helper';
import { AccountBalancesResponse } from '@ghostfolio/common/interfaces'; import { AccountBalancesResponse } from '@ghostfolio/common/interfaces';
import { import {
@ -25,7 +26,7 @@ import { Subject } from 'rxjs';
export class AccountBalancesComponent implements OnChanges, OnDestroy, OnInit { export class AccountBalancesComponent implements OnChanges, OnDestroy, OnInit {
@Input() accountBalances: AccountBalancesResponse['balances']; @Input() accountBalances: AccountBalancesResponse['balances'];
@Input() accountId: string; @Input() accountId: string;
@Input() locale: string; @Input() locale = getLocale();
@Input() showActions = true; @Input() showActions = true;
@Output() accountBalanceDeleted = new EventEmitter<string>(); @Output() accountBalanceDeleted = new EventEmitter<string>();

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

@ -1,6 +1,6 @@
import { Activity } from '@ghostfolio/api/app/order/interfaces/activities.interface'; import { Activity } from '@ghostfolio/api/app/order/interfaces/activities.interface';
import { DEFAULT_PAGE_SIZE } from '@ghostfolio/common/config'; import { DEFAULT_PAGE_SIZE } from '@ghostfolio/common/config';
import { getDateFormatString } from '@ghostfolio/common/helper'; import { getDateFormatString, getLocale } from '@ghostfolio/common/helper';
import { UniqueAsset } from '@ghostfolio/common/interfaces'; import { UniqueAsset } from '@ghostfolio/common/interfaces';
import { OrderWithAccount } from '@ghostfolio/common/types'; import { OrderWithAccount } from '@ghostfolio/common/types';
@ -40,7 +40,7 @@ export class ActivitiesTableComponent
@Input() hasPermissionToCreateActivity: boolean; @Input() hasPermissionToCreateActivity: boolean;
@Input() hasPermissionToExportActivities: boolean; @Input() hasPermissionToExportActivities: boolean;
@Input() hasPermissionToOpenDetails = true; @Input() hasPermissionToOpenDetails = true;
@Input() locale: string; @Input() locale = getLocale();
@Input() pageIndex: number; @Input() pageIndex: number;
@Input() pageSize = DEFAULT_PAGE_SIZE; @Input() pageSize = DEFAULT_PAGE_SIZE;
@Input() showActions = true; @Input() showActions = true;

9
libs/ui/src/lib/assistant/assistant-list-item/assistant-list-item.html

@ -4,12 +4,13 @@
[queryParams]="queryParams" [queryParams]="queryParams"
[routerLink]="routerLink" [routerLink]="routerLink"
(click)="onClick()" (click)="onClick()"
><span><b>{{ item?.name }}</b></span> ><span
><b>{{ item?.name }}</b></span
>
<br /> <br />
<small class="text-muted" <small class="text-muted"
>{{ item?.symbol | gfSymbol }} · {{ item?.currency }}<ng-container >{{ item?.symbol | gfSymbol }} · {{ item?.currency
*ngIf="item?.assetSubClassString" }}<ng-container *ngIf="item?.assetSubClassString">
>
· {{ item?.assetSubClassString }}</ng-container · {{ item?.assetSubClassString }}</ng-container
></small ></small
></a ></a

46
libs/ui/src/lib/assistant/assistant.html

@ -39,7 +39,7 @@
</button> </button>
</div> </div>
<div <div
*ngIf="isLoading || searchFormControl.value" *ngIf="isLoading || searchFormControl.value"
class="overflow-auto py-3 result-container" class="overflow-auto py-3 result-container"
> >
<div> <div>
@ -56,9 +56,9 @@
animation="pulse" animation="pulse"
class="mx-2" class="mx-2"
[theme]="{ [theme]="{
height: '1.5rem', height: '1.5rem',
width: '100%' width: '100%'
}" }"
/> />
<div *ngIf="!isLoading" class="px-2 py-1" i18n>No entries...</div> <div *ngIf="!isLoading" class="px-2 py-1" i18n>No entries...</div>
</ng-container> </ng-container>
@ -77,9 +77,9 @@
animation="pulse" animation="pulse"
class="mx-2" class="mx-2"
[theme]="{ [theme]="{
height: '1.5rem', height: '1.5rem',
width: '100%' width: '100%'
}" }"
/> />
<div *ngIf="!isLoading" class="px-2 py-1" i18n>No entries...</div> <div *ngIf="!isLoading" class="px-2 py-1" i18n>No entries...</div>
</ng-container> </ng-container>
@ -87,7 +87,7 @@
</div> </div>
</div> </div>
<form [formGroup]="filterForm"> <form [formGroup]="filterForm">
<ng-container *ngIf="!(isLoading || searchFormControl.value)"> <ng-container *ngIf="!(isLoading || searchFormControl.value)">
<div class="date-range-selector-container p-3"> <div class="date-range-selector-container p-3">
<mat-form-field appearance="outline" class="w-100 without-hint"> <mat-form-field appearance="outline" class="w-100 without-hint">
<mat-label i18n>Date Range</mat-label> <mat-label i18n>Date Range</mat-label>
@ -96,7 +96,7 @@
(selectionChange)="onChangeDateRange($event.value)" (selectionChange)="onChangeDateRange($event.value)"
> >
@for (range of dateRangeOptions; track range) { @for (range of dateRangeOptions; track range) {
<mat-option [value]="range.value">{{ range.label }}</mat-option> <mat-option [value]="range.value">{{ range.label }}</mat-option>
} }
</mat-select> </mat-select>
</mat-form-field> </mat-form-field>
@ -108,16 +108,16 @@
<mat-select formControlName="account"> <mat-select formControlName="account">
<mat-option [value]="null" /> <mat-option [value]="null" />
@for (account of accounts; track account.id) { @for (account of accounts; track account.id) {
<mat-option [value]="account.id"> <mat-option [value]="account.id">
<div class="d-flex"> <div class="d-flex">
<gf-symbol-icon <gf-symbol-icon
*ngIf="account.Platform?.url" *ngIf="account.Platform?.url"
class="mr-1" class="mr-1"
[tooltip]="account.Platform?.name" [tooltip]="account.Platform?.name"
[url]="account.Platform?.url" [url]="account.Platform?.url"
/><span>{{ account.name }}</span> /><span>{{ account.name }}</span>
</div> </div>
</mat-option> </mat-option>
} }
</mat-select> </mat-select>
</mat-form-field> </mat-form-field>
@ -128,7 +128,7 @@
<mat-select formControlName="tag"> <mat-select formControlName="tag">
<mat-option [value]="null" /> <mat-option [value]="null" />
@for (tag of tags; track tag.id) { @for (tag of tags; track tag.id) {
<mat-option [value]="tag.id">{{ tag.label }}</mat-option> <mat-option [value]="tag.id">{{ tag.label }}</mat-option>
} }
</mat-select> </mat-select>
</mat-form-field> </mat-form-field>
@ -139,9 +139,9 @@
<mat-select formControlName="assetClass"> <mat-select formControlName="assetClass">
<mat-option [value]="null" /> <mat-option [value]="null" />
@for (assetClass of assetClasses; track assetClass.id) { @for (assetClass of assetClasses; track assetClass.id) {
<mat-option [value]="assetClass.id" <mat-option [value]="assetClass.id">{{
>{{ assetClass.label }}</mat-option assetClass.label
> }}</mat-option>
} }
</mat-select> </mat-select>
</mat-form-field> </mat-form-field>

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

@ -1,4 +1,4 @@
import { resolveMarketCondition } from '@ghostfolio/common/helper'; import { getLocale, resolveMarketCondition } from '@ghostfolio/common/helper';
import { Benchmark, User } from '@ghostfolio/common/interfaces'; import { Benchmark, User } from '@ghostfolio/common/interfaces';
import { import {
@ -16,7 +16,7 @@ import {
}) })
export class BenchmarkComponent implements OnChanges { export class BenchmarkComponent implements OnChanges {
@Input() benchmarks: Benchmark[]; @Input() benchmarks: Benchmark[];
@Input() locale: string; @Input() locale = getLocale();
@Input() user: User; @Input() user: User;
public displayedColumns = ['name', 'date', 'change', 'marketCondition']; public displayedColumns = ['name', 'date', 'change', 'marketCondition'];

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

@ -3,6 +3,7 @@ import {
transformTickToAbbreviation transformTickToAbbreviation
} from '@ghostfolio/common/chart-helper'; } from '@ghostfolio/common/chart-helper';
import { primaryColorRgb } from '@ghostfolio/common/config'; import { primaryColorRgb } from '@ghostfolio/common/config';
import { getLocale } from '@ghostfolio/common/helper';
import { ColorScheme } from '@ghostfolio/common/types'; import { ColorScheme } from '@ghostfolio/common/types';
import { import {
@ -55,7 +56,7 @@ export class FireCalculatorComponent implements OnChanges, OnDestroy {
@Input() deviceType: string; @Input() deviceType: string;
@Input() fireWealth: number; @Input() fireWealth: number;
@Input() hasPermissionToUpdateUserSettings: boolean; @Input() hasPermissionToUpdateUserSettings: boolean;
@Input() locale: string; @Input() locale = getLocale();
@Input() projectedTotalAmount = 0; @Input() projectedTotalAmount = 0;
@Input() retirementDate: Date; @Input() retirementDate: Date;
@Input() savingsRate = 0; @Input() savingsRate = 0;

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

@ -1,3 +1,4 @@
import { getLocale } from '@ghostfolio/common/helper';
import { PortfolioPosition, UniqueAsset } from '@ghostfolio/common/interfaces'; import { PortfolioPosition, UniqueAsset } from '@ghostfolio/common/interfaces';
import { import {
@ -29,7 +30,7 @@ export class HoldingsTableComponent implements OnChanges, OnDestroy, OnInit {
@Input() hasPermissionToOpenDetails = true; @Input() hasPermissionToOpenDetails = true;
@Input() hasPermissionToShowValues = true; @Input() hasPermissionToShowValues = true;
@Input() holdings: PortfolioPosition[]; @Input() holdings: PortfolioPosition[];
@Input() locale: string; @Input() locale = getLocale();
@Input() pageSize = Number.MAX_SAFE_INTEGER; @Input() pageSize = Number.MAX_SAFE_INTEGER;
@ViewChild(MatPaginator) paginator: MatPaginator; @ViewChild(MatPaginator) paginator: MatPaginator;

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

@ -3,14 +3,11 @@ import {
getTooltipPositionerMapTop, getTooltipPositionerMapTop,
getVerticalHoverLinePlugin getVerticalHoverLinePlugin
} from '@ghostfolio/common/chart-helper'; } from '@ghostfolio/common/chart-helper';
import { import { primaryColorRgb, secondaryColorRgb } from '@ghostfolio/common/config';
locale,
primaryColorRgb,
secondaryColorRgb
} from '@ghostfolio/common/config';
import { import {
getBackgroundColor, getBackgroundColor,
getDateFormatString, getDateFormatString,
getLocale,
getTextColor getTextColor
} from '@ghostfolio/common/helper'; } from '@ghostfolio/common/helper';
import { LineChartItem } from '@ghostfolio/common/interfaces'; import { LineChartItem } from '@ghostfolio/common/interfaces';
@ -51,7 +48,7 @@ export class LineChartComponent implements AfterViewInit, OnChanges, OnDestroy {
@Input() currency: string; @Input() currency: string;
@Input() historicalDataItems: LineChartItem[]; @Input() historicalDataItems: LineChartItem[];
@Input() isAnimated = false; @Input() isAnimated = false;
@Input() locale: string; @Input() locale = getLocale();
@Input() showGradient = false; @Input() showGradient = false;
@Input() showLegend = false; @Input() showLegend = false;
@Input() showLoader = true; @Input() showLoader = true;
@ -106,10 +103,6 @@ export class LineChartComponent implements AfterViewInit, OnChanges, OnDestroy {
this.changeDetectorRef.markForCheck(); this.changeDetectorRef.markForCheck();
}); });
} }
if (!this.locale) {
this.locale = locale;
}
} }
public ngOnDestroy() { public ngOnDestroy() {

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

@ -1,6 +1,6 @@
import { getTooltipOptions } from '@ghostfolio/common/chart-helper'; import { getTooltipOptions } from '@ghostfolio/common/chart-helper';
import { UNKNOWN_KEY } from '@ghostfolio/common/config'; import { UNKNOWN_KEY } from '@ghostfolio/common/config';
import { getTextColor } from '@ghostfolio/common/helper'; import { getLocale, getTextColor } from '@ghostfolio/common/helper';
import { PortfolioPosition, UniqueAsset } from '@ghostfolio/common/interfaces'; import { PortfolioPosition, UniqueAsset } from '@ghostfolio/common/interfaces';
import { ColorScheme } from '@ghostfolio/common/types'; import { ColorScheme } from '@ghostfolio/common/types';
import { translate } from '@ghostfolio/ui/i18n'; import { translate } from '@ghostfolio/ui/i18n';
@ -41,7 +41,7 @@ export class PortfolioProportionChartComponent
@Input() cursor: string; @Input() cursor: string;
@Input() isInPercent = false; @Input() isInPercent = false;
@Input() keys: string[] = []; @Input() keys: string[] = [];
@Input() locale = ''; @Input() locale = getLocale();
@Input() maxItems?: number; @Input() maxItems?: number;
@Input() showLabels = false; @Input() showLabels = false;
@Input() positions: { @Input() positions: {

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

@ -21,7 +21,7 @@ export class ValueComponent implements OnChanges {
@Input() isCurrency = false; @Input() isCurrency = false;
@Input() isDate = false; @Input() isDate = false;
@Input() isPercent = false; @Input() isPercent = false;
@Input() locale: string | undefined; @Input() locale = getLocale();
@Input() position = ''; @Input() position = '';
@Input() precision: number | undefined; @Input() precision: number | undefined;
@Input() size: 'large' | 'medium' | 'small' = 'small'; @Input() size: 'large' | 'medium' | 'small' = 'small';
@ -129,11 +129,6 @@ export class ValueComponent implements OnChanges {
this.formattedValue = ''; this.formattedValue = '';
this.isNumber = false; this.isNumber = false;
this.isString = false; this.isString = false;
if (!this.locale) {
this.locale = getLocale();
}
this.useAbsoluteValue = false; this.useAbsoluteValue = false;
} }
} }

Loading…
Cancel
Save