Compare commits

...

3 Commits

Author SHA1 Message Date
github-actions[bot] 5d4ae49a38 Update locales 3 days ago
Thomas Kaul 108e29444a
Task/add scopes to access (#7603) 3 days ago
David Requeno 9d2e4b732c
Feature/add stock splits logic (#7568) 3 days ago
  1. 15
      CHANGELOG.md
  2. 42
      apps/api/src/app/access/access.controller.ts
  3. 9
      apps/api/src/app/account/account.controller.ts
  4. 7
      apps/api/src/app/activities/activities.controller.ts
  5. 2
      apps/api/src/app/activities/activities.module.ts
  6. 337
      apps/api/src/app/activities/activities.service.spec.ts
  7. 48
      apps/api/src/app/activities/activities.service.ts
  8. 7
      apps/api/src/app/endpoints/asset-profiles/asset-profiles.controller.ts
  9. 179
      apps/api/src/app/endpoints/asset-profiles/asset-profiles.service.spec.ts
  10. 56
      apps/api/src/app/endpoints/asset-profiles/asset-profiles.service.ts
  11. 5
      apps/api/src/app/endpoints/benchmarks/benchmarks.controller.ts
  12. 5
      apps/api/src/app/endpoints/watchlist/watchlist.controller.ts
  13. 4
      apps/api/src/app/portfolio/calculator/roai/portfolio-calculator-cash.spec.ts
  14. 276
      apps/api/src/app/portfolio/calculator/roai/portfolio-calculator-stock-split.spec.ts
  15. 70
      apps/api/src/app/portfolio/portfolio.controller.ts
  16. 2
      apps/api/src/app/portfolio/portfolio.service.spec.ts
  17. 3
      apps/api/src/app/user/user.controller.ts
  18. 10
      apps/api/src/app/user/user.service.ts
  19. 2
      apps/api/src/decorators/impersonation.decorator.ts
  20. 41
      apps/api/src/decorators/requires-scope.decorator.spec.ts
  21. 26
      apps/api/src/decorators/requires-scope.decorator.ts
  22. 50
      apps/api/src/guards/scope.guard.ts
  23. 29
      apps/api/src/interceptors/redact-values-in-response/redact-values-in-response.interceptor.ts
  24. 114
      apps/api/src/services/asset-profile-split/asset-profile-split.helper.spec.ts
  25. 58
      apps/api/src/services/asset-profile-split/asset-profile-split.helper.ts
  26. 161
      apps/api/src/services/asset-profile-split/asset-profile-split.service.spec.ts
  27. 27
      apps/api/src/services/asset-profile-split/asset-profile-split.service.ts
  28. 32
      apps/api/src/services/impersonation/impersonation.service.ts
  29. 8
      apps/api/src/services/queues/data-gathering/data-gathering.service.ts
  30. 4
      apps/client/src/app/components/access-table/access-table.component.html
  31. 5
      apps/client/src/app/components/access-table/access-table.component.ts
  32. 13
      apps/client/src/app/components/account-detail-dialog/account-detail-dialog.component.ts
  33. 4
      apps/client/src/app/components/admin-market-data/asset-profile-dialog/asset-profile-dialog.html
  34. 3
      apps/client/src/app/components/user-account-access/create-or-update-access-dialog/interfaces/interfaces.ts
  35. 21
      apps/client/src/app/components/user-account-access/user-account-access.component.ts
  36. 13
      apps/client/src/app/pages/portfolio/allocations/allocations-page.component.ts
  37. 27
      apps/client/src/app/pages/portfolio/analysis/analysis-page.component.ts
  38. 60
      apps/client/src/locales/messages.ca.xlf
  39. 60
      apps/client/src/locales/messages.de.xlf
  40. 60
      apps/client/src/locales/messages.es.xlf
  41. 60
      apps/client/src/locales/messages.fr.xlf
  42. 60
      apps/client/src/locales/messages.it.xlf
  43. 60
      apps/client/src/locales/messages.ja.xlf
  44. 60
      apps/client/src/locales/messages.ko.xlf
  45. 60
      apps/client/src/locales/messages.nl.xlf
  46. 60
      apps/client/src/locales/messages.pl.xlf
  47. 60
      apps/client/src/locales/messages.pt.xlf
  48. 60
      apps/client/src/locales/messages.tr.xlf
  49. 60
      apps/client/src/locales/messages.uk.xlf
  50. 60
      apps/client/src/locales/messages.xlf
  51. 60
      apps/client/src/locales/messages.zh.xlf
  52. 3
      libs/common/src/lib/dtos/create-access.dto.ts
  53. 3
      libs/common/src/lib/dtos/update-access.dto.ts
  54. 6
      libs/common/src/lib/interfaces/access.interface.ts
  55. 3
      libs/common/src/lib/interfaces/user.interface.ts
  56. 25
      libs/common/src/lib/permissions.ts
  57. 130
      libs/common/src/lib/scopes.spec.ts
  58. 78
      libs/common/src/lib/scopes.ts
  59. 1
      libs/common/src/lib/types/impersonation-context.type.ts
  60. 3
      libs/common/src/lib/types/user-with-settings.type.ts
  61. 2
      prisma/migrations/20260815120000_added_symbol_profile_id_index_to_order/migration.sql
  62. 24
      prisma/migrations/20260815130000_added_scopes_to_access/migration.sql
  63. 3
      prisma/schema.prisma

15
CHANGELOG.md

@ -5,6 +5,21 @@ All notable changes to this project will be documented in this file.
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
## Unreleased
### Added
- Added the business logic and tests for stock splits of an asset profile (experimental)
- Added the scopes to the access
- Added an index for `symbolProfileId` to the order database table
### Changed
- Changed the redaction of the monetary values in impersonation mode to be based on the scopes of the access
- Deprecated the `permissions` attribute of the access in favor of the scopes
- Extended the `GET api/v1/access` endpoint by the scopes
- Extended the `GET api/v1/user` endpoint by the scopes
## 3.51.0 - 2026-08-14
### Changed

42
apps/api/src/app/access/access.controller.ts

@ -6,6 +6,7 @@ import { CreateAccessDto, UpdateAccessDto } from '@ghostfolio/common/dtos';
import { SubscriptionType } from '@ghostfolio/common/enums';
import { Access, AccessSettings } from '@ghostfolio/common/interfaces';
import { permissions } from '@ghostfolio/common/permissions';
import { getScopesOfAccess } from '@ghostfolio/common/scopes';
import type { RequestWithUser } from '@ghostfolio/common/types';
import {
@ -47,29 +48,32 @@ export class AccessController {
where: { userId: this.request.user.id }
});
return accessesWithGranteeUser.map(
({ alias, granteeUser, id, permissions, settings }) => {
if (granteeUser) {
return {
alias,
id,
permissions,
grantee: granteeUser?.id,
settings: settings as AccessSettings,
type: 'PRIVATE'
};
}
return accessesWithGranteeUser.map((accessItem) => {
const { alias, granteeUser, id, permissions, settings } = accessItem;
const scopes = getScopesOfAccess(accessItem);
if (granteeUser) {
return {
alias,
id,
permissions,
grantee: 'Public',
scopes,
grantee: granteeUser?.id,
settings: settings as AccessSettings,
type: 'PUBLIC'
type: 'PRIVATE'
};
}
);
return {
alias,
id,
permissions,
scopes,
grantee: 'Public',
settings: settings as AccessSettings,
type: 'PUBLIC'
};
});
}
@HasPermission(permissions.createAccess)
@ -95,6 +99,10 @@ export class AccessController {
? { connect: { id: data.granteeUserId } }
: undefined,
permissions: data.permissions,
scopes: getScopesOfAccess({
granteeUserId: data.granteeUserId,
permissions: data.permissions
}),
settings: this.accessService.buildSettings(data.filters),
user: { connect: { id: this.request.user.id } }
});
@ -164,6 +172,10 @@ export class AccessController {
? { connect: { id: data.granteeUserId } }
: { disconnect: true },
permissions: data.permissions,
scopes: getScopesOfAccess({
granteeUserId: data.granteeUserId,
permissions: data.permissions ?? originalAccess.permissions
}),
settings: this.accessService.buildSettings(data.filters)
},
where: { id }

9
apps/api/src/app/account/account.controller.ts

@ -2,8 +2,8 @@ import { AccountBalanceService } from '@ghostfolio/api/app/account-balance/accou
import { PortfolioService } from '@ghostfolio/api/app/portfolio/portfolio.service';
import { HasPermission } from '@ghostfolio/api/decorators/has-permission.decorator';
import { Impersonation } from '@ghostfolio/api/decorators/impersonation.decorator';
import { RequiresScope } from '@ghostfolio/api/decorators/requires-scope.decorator';
import { HasPermissionGuard } from '@ghostfolio/api/guards/has-permission.guard';
import { ImpersonationGuard } from '@ghostfolio/api/guards/impersonation.guard';
import { RedactValuesInResponseInterceptor } from '@ghostfolio/api/interceptors/redact-values-in-response/redact-values-in-response.interceptor';
import { TransformDataSourceInRequestInterceptor } from '@ghostfolio/api/interceptors/transform-data-source-in-request/transform-data-source-in-request.interceptor';
import { ApiService } from '@ghostfolio/api/services/api/api.service';
@ -18,6 +18,7 @@ import {
AccountsResponse
} from '@ghostfolio/common/interfaces';
import { permissions } from '@ghostfolio/common/permissions';
import { scopes } from '@ghostfolio/common/scopes';
import type {
ImpersonationContext,
RequestWithUser
@ -84,7 +85,7 @@ export class AccountController {
}
@Get()
@UseGuards(AuthGuard('jwt'), HasPermissionGuard, ImpersonationGuard)
@RequiresScope(scopes.accountRead)
@UseInterceptors(RedactValuesInResponseInterceptor)
@UseInterceptors(TransformDataSourceInRequestInterceptor)
public async getAllAccounts(
@ -107,7 +108,7 @@ export class AccountController {
}
@Get(':id')
@UseGuards(AuthGuard('jwt'), HasPermissionGuard, ImpersonationGuard)
@RequiresScope(scopes.accountRead)
@UseInterceptors(RedactValuesInResponseInterceptor)
public async getAccountById(
@Impersonation() { userId }: ImpersonationContext,
@ -124,7 +125,7 @@ export class AccountController {
}
@Get(':id/balances')
@UseGuards(AuthGuard('jwt'), HasPermissionGuard, ImpersonationGuard)
@RequiresScope(scopes.accountRead)
@UseInterceptors(RedactValuesInResponseInterceptor)
public async getAccountBalancesById(
@Impersonation() { userId, userSettings }: ImpersonationContext,

7
apps/api/src/app/activities/activities.controller.ts

@ -1,7 +1,7 @@
import { HasPermission } from '@ghostfolio/api/decorators/has-permission.decorator';
import { Impersonation } from '@ghostfolio/api/decorators/impersonation.decorator';
import { RequiresScope } from '@ghostfolio/api/decorators/requires-scope.decorator';
import { HasPermissionGuard } from '@ghostfolio/api/guards/has-permission.guard';
import { ImpersonationGuard } from '@ghostfolio/api/guards/impersonation.guard';
import { isActivityInFuture } from '@ghostfolio/api/helper/activity.helper';
import { RedactValuesInResponseInterceptor } from '@ghostfolio/api/interceptors/redact-values-in-response/redact-values-in-response.interceptor';
import { TransformDataSourceInRequestInterceptor } from '@ghostfolio/api/interceptors/transform-data-source-in-request/transform-data-source-in-request.interceptor';
@ -17,6 +17,7 @@ import {
ActivityResponse
} from '@ghostfolio/common/interfaces';
import { permissions } from '@ghostfolio/common/permissions';
import { scopes } from '@ghostfolio/common/scopes';
import type {
ImpersonationContext,
RequestWithUser
@ -120,7 +121,7 @@ export class ActivitiesController {
}
@Get()
@UseGuards(AuthGuard('jwt'), HasPermissionGuard, ImpersonationGuard)
@RequiresScope(scopes.activityRead)
@UseInterceptors(RedactValuesInResponseInterceptor)
@UseInterceptors(TransformDataSourceInRequestInterceptor)
@UseInterceptors(TransformDataSourceInResponseInterceptor)
@ -177,7 +178,7 @@ export class ActivitiesController {
}
@Get(':id')
@UseGuards(AuthGuard('jwt'), HasPermissionGuard, ImpersonationGuard)
@RequiresScope(scopes.activityRead)
@UseInterceptors(RedactValuesInResponseInterceptor)
@UseInterceptors(TransformDataSourceInResponseInterceptor)
public async getActivityById(

2
apps/api/src/app/activities/activities.module.ts

@ -6,6 +6,7 @@ import { RedactValuesInResponseModule } from '@ghostfolio/api/interceptors/redac
import { TransformDataSourceInRequestModule } from '@ghostfolio/api/interceptors/transform-data-source-in-request/transform-data-source-in-request.module';
import { TransformDataSourceInResponseModule } from '@ghostfolio/api/interceptors/transform-data-source-in-response/transform-data-source-in-response.module';
import { ApiModule } from '@ghostfolio/api/services/api/api.module';
import { AssetProfileSplitModule } from '@ghostfolio/api/services/asset-profile-split/asset-profile-split.module';
import { BenchmarkModule } from '@ghostfolio/api/services/benchmark/benchmark.module';
import { DataProviderModule } from '@ghostfolio/api/services/data-provider/data-provider.module';
import { ExchangeRateDataModule } from '@ghostfolio/api/services/exchange-rate-data/exchange-rate-data.module';
@ -26,6 +27,7 @@ import { ActivitiesService } from './activities.service';
exports: [ActivitiesService],
imports: [
ApiModule,
AssetProfileSplitModule,
BenchmarkModule,
CacheModule,
DataGatheringQueueModule,

337
apps/api/src/app/activities/activities.service.spec.ts

@ -0,0 +1,337 @@
import { AccountService } from '@ghostfolio/api/app/account/account.service';
import {
activityDummyData,
assetProfileDummyData
} from '@ghostfolio/api/app/portfolio/calculator/portfolio-calculator-test-utils';
import { AssetProfileSplitService } from '@ghostfolio/api/services/asset-profile-split/asset-profile-split.service';
import {
INVESTMENT_ACTIVITY_TYPES,
NON_INVESTMENT_ACTIVITY_TYPES
} from '@ghostfolio/common/config';
import { parseDate } from '@ghostfolio/common/helper';
import { Activity, Filter } from '@ghostfolio/common/interfaces';
import { AssetProfileSplit, DataSource } from '@prisma/client';
import { Big } from 'big.js';
import { ActivitiesService } from './activities.service';
describe('ActivitiesService', () => {
let activitiesService: ActivitiesService;
let getSplitsByUserId: jest.Mock;
let accountService: { getCashDetails: jest.Mock };
beforeEach(() => {
getSplitsByUserId = jest.fn().mockResolvedValue([]);
accountService = { getCashDetails: jest.fn() };
activitiesService = new ActivitiesService(
null,
accountService as unknown as AccountService,
{ getSplitsByUserId } as unknown as AssetProfileSplitService,
null,
null,
null,
null,
null,
null,
null,
null,
null
);
});
describe('getActivitiesForPortfolioCalculator', () => {
it('leaves an activity unchanged when no splits exist', async () => {
const activity = createActivity({ symbol: 'AAPL' });
const result = await getAdjustedActivity(activity, []);
expect(result).toEqual(activity);
expect(result).toBe(activity);
});
it.each([
{ denominator: 1, expectedPrice: 50, expectedQuantity: 20, numerator: 2 },
{
denominator: 10,
expectedPrice: 1000,
expectedQuantity: 1,
numerator: 1
}
])(
'applies a $numerator:$denominator split to quantity and price',
async ({ denominator, expectedPrice, expectedQuantity, numerator }) => {
const result = await getAdjustedActivity(
createActivity({ symbol: 'AAPL' }),
[createSplit('2021-01-01', numerator, denominator)]
);
expect(result.quantity).toBe(expectedQuantity);
expect(result.unitPrice).toBe(expectedPrice);
expect(result.unitPriceInAssetProfileCurrency).toBe(expectedPrice);
}
);
it('adjusts only activities before the split calendar date', async () => {
const split = createSplit('2021-01-01', 2, 1);
const activityBeforeSplit = createActivity({
date: '2020-12-31T23:00:00.000Z',
symbol: 'AAPL'
});
const activityOnSplitDate = createActivity({
date: '2021-01-01T23:00:00.000Z',
symbol: 'AAPL'
});
const activityAfterSplit = createActivity({
date: '2021-01-02T00:00:00.000Z',
symbol: 'AAPL'
});
expect(
(await getAdjustedActivity(activityBeforeSplit, [split])).quantity
).toBe(20);
expect(
(await getAdjustedActivity(activityOnSplitDate, [split])).quantity
).toBe(10);
expect(
(await getAdjustedActivity(activityAfterSplit, [split])).quantity
).toBe(10);
});
it('applies multiple splits cumulatively with exact ratio arithmetic', async () => {
const result = await getAdjustedActivity(
createActivity({ symbol: 'AAPL' }),
[createSplit('2021-01-01', 2, 1), createSplit('2022-01-01', 1, 3)]
);
expect(new Big(result.quantity).toFixed(15)).toBe(
new Big(20).div(3).toFixed(15)
);
expect(result.unitPrice).toBe(150);
});
it('preserves fees and total activity value', async () => {
const activity = createActivity({ symbol: 'AAPL' });
activity.feeInAssetProfileCurrency = 12;
activity.feeInBaseCurrency = 15;
const result = await getAdjustedActivity(activity, [
createSplit('2021-01-01', 2, 1)
]);
expect(result).toMatchObject({
feeInAssetProfileCurrency: 12,
feeInBaseCurrency: 15,
value: 1000,
valueInBaseCurrency: 1000
});
expect(result.quantity * result.unitPrice).toBe(1000);
});
it('does not apply splits from another symbol or data source', async () => {
const activity = createActivity({ symbol: 'AAPL' });
const split = createSplit('2021-01-01', 2, 1);
jest.spyOn(activitiesService, 'getActivities').mockResolvedValue({
activities: [activity],
count: 1
});
getSplitsByUserId.mockResolvedValue([
{ ...split, symbolProfileId: 'YAHOO-MSFT-profile' },
{ ...split, symbolProfileId: 'MANUAL-AAPL-profile' }
]);
const result =
await activitiesService.getActivitiesForPortfolioCalculator({
userCurrency: 'USD',
userId: 'user-id'
});
expect(result.activities[0].quantity).toBe(10);
expect(result.activities[0].unitPrice).toBe(100);
});
it.each(INVESTMENT_ACTIVITY_TYPES)(
'adjusts %s activities',
async (type) => {
const activity = createActivity({ symbol: 'AAPL' });
activity.type = type as Activity['type'];
const result = await getAdjustedActivity(activity, [
createSplit('2021-01-01', 2, 1)
]);
expect(result.quantity).toBe(20);
expect(result.unitPrice).toBe(50);
expect(result.quantity * result.unitPrice).toBe(1000);
}
);
it.each(NON_INVESTMENT_ACTIVITY_TYPES)(
'leaves %s activities unchanged',
async (type) => {
const activity = createActivity({ symbol: 'AAPL' });
activity.type = type as Activity['type'];
const result = await getAdjustedActivity(activity, [
createSplit('2021-01-01', 2, 1)
]);
expect(result).toBe(activity);
expect(result.quantity).toBe(10);
expect(result.unitPrice).toBe(100);
}
);
it('loads and applies splits to standard activities while preserving filters', async () => {
const activity = createActivity({ symbol: 'AAPL' });
const filters = [{ id: 'AAPL', type: 'SYMBOL' }] as Filter[];
const split = createSplit();
jest.spyOn(activitiesService, 'getActivities').mockResolvedValue({
activities: [activity],
count: 1
});
getSplitsByUserId.mockResolvedValue([split]);
const result =
await activitiesService.getActivitiesForPortfolioCalculator({
filters,
userCurrency: 'USD',
userId: 'user-id'
});
expect(activitiesService.getActivities).toHaveBeenCalledWith({
filters,
userCurrency: 'USD',
userId: 'user-id',
withExcludedAccountsAndActivities: false
});
expect(getSplitsByUserId).toHaveBeenCalledWith({ userId: 'user-id' });
expect(result.activities[0]).toMatchObject({
quantity: 20,
unitPrice: 50,
unitPriceInAssetProfileCurrency: 50
});
});
it('does not adjust synthetic cash activities', async () => {
const activity = createActivity({ symbol: 'AAPL' });
const cashActivity = createActivity({
assetSubClass: 'CASH',
currency: 'USD',
dataSource: DataSource.YAHOO,
quantity: 100,
symbol: 'USD',
unitPrice: 1
});
const split = createSplit();
jest.spyOn(activitiesService, 'getActivities').mockResolvedValue({
activities: [activity],
count: 1
});
jest.spyOn(activitiesService, 'getCashActivities').mockResolvedValue({
activities: [cashActivity],
count: 1
});
accountService.getCashDetails.mockResolvedValue({ accounts: [] });
getSplitsByUserId.mockResolvedValue([split]);
const result =
await activitiesService.getActivitiesForPortfolioCalculator({
userCurrency: 'USD',
userId: 'user-id',
withCash: true
});
expect(getSplitsByUserId).toHaveBeenCalledWith({ userId: 'user-id' });
expect(result.activities).toEqual([
expect.objectContaining({
assetProfile: expect.objectContaining({ symbol: 'AAPL' }),
quantity: 20
}),
cashActivity
]);
});
async function getAdjustedActivity(
activity: Activity,
splits: AssetProfileSplit[]
) {
jest.spyOn(activitiesService, 'getActivities').mockResolvedValue({
activities: [activity],
count: 1
});
getSplitsByUserId.mockResolvedValue(
splits.map((split) => {
return { ...split, symbolProfileId: activity.assetProfile.id };
})
);
const result =
await activitiesService.getActivitiesForPortfolioCalculator({
userCurrency: 'USD',
userId: 'user-id'
});
return result.activities[0];
}
});
});
function createActivity({
assetSubClass,
currency,
dataSource = DataSource.YAHOO,
date = '2020-01-01',
quantity = 10,
symbol,
unitPrice = 100
}: {
assetSubClass?: string;
currency?: string;
dataSource?: DataSource;
date?: string;
quantity?: number;
symbol: string;
unitPrice?: number;
}): Activity {
return {
...activityDummyData,
assetProfile: {
...assetProfileDummyData,
assetSubClass,
currency,
dataSource,
id: `${dataSource}-${symbol}-profile`,
symbol
},
date: parseDate(date),
quantity,
type: 'BUY',
unitPrice,
unitPriceInAssetProfileCurrency: unitPrice,
value: quantity * unitPrice,
valueInBaseCurrency: quantity * unitPrice
} as Activity;
}
function createSplit(
dateString = '2021-01-01',
numerator = 2,
denominator = 1
): AssetProfileSplit {
const date = parseDate(dateString);
return {
createdAt: date,
date,
denominator,
id: `${dateString}-${numerator}-${denominator}`,
numerator,
symbolProfileId: 'YAHOO-AAPL-profile',
updatedAt: date
};
}

48
apps/api/src/app/activities/activities.service.ts

@ -14,6 +14,8 @@ import {
WHERE_ACTIVITY_NOT_DRAFT
} from '@ghostfolio/api/helper/activity.helper';
import { LogPerformance } from '@ghostfolio/api/interceptors/performance-logging/performance-logging.interceptor';
import { adjustActivityBySplits } from '@ghostfolio/api/services/asset-profile-split/asset-profile-split.helper';
import { AssetProfileSplitService } from '@ghostfolio/api/services/asset-profile-split/asset-profile-split.service';
import { BenchmarkService } from '@ghostfolio/api/services/benchmark/benchmark.service';
import { DataProviderService } from '@ghostfolio/api/services/data-provider/data-provider.service';
import { ExchangeRateDataService } from '@ghostfolio/api/services/exchange-rate-data/exchange-rate-data.service';
@ -66,6 +68,7 @@ export class ActivitiesService {
public constructor(
private readonly accountBalanceService: AccountBalanceService,
private readonly accountService: AccountService,
private readonly assetProfileSplitService: AssetProfileSplitService,
private readonly benchmarkService: BenchmarkService,
private readonly dataGatheringService: DataGatheringService,
private readonly dataProviderService: DataProviderService,
@ -915,12 +918,26 @@ export class ActivitiesService {
/** Whether to include cash activities in the result. */
withCash?: boolean;
}) {
const activities = await this.getActivities({
filters,
userCurrency,
userId,
withExcludedAccountsAndActivities: false // TODO
});
const [activities, splits] = await Promise.all([
this.getActivities({
filters,
userCurrency,
userId,
withExcludedAccountsAndActivities: false // TODO
}),
this.assetProfileSplitService.getSplitsByUserId({ userId })
]);
if (splits.length > 0) {
const splitsBySymbolProfileId = groupBy(splits, 'symbolProfileId');
activities.activities = activities.activities.map((activity) => {
return adjustActivityBySplits(
activity,
splitsBySymbolProfileId[activity.assetProfile.id] ?? []
);
});
}
if (withCash && !this.areCashActivitiesExcludedByFilters(filters)) {
const cashDetails = await this.accountService.getCashDetails({
@ -963,6 +980,25 @@ export class ActivitiesService {
};
}
/**
* Returns the id of every user who has an activity for the given asset
* profile, including draft activities and activities of excluded accounts
*/
public async getUserIdsBySymbolProfileId(
symbolProfileId: string
): Promise<string[]> {
const activitiesByUser = await this.prismaService.order.groupBy({
by: ['userId'],
where: {
symbolProfileId
}
});
return activitiesByUser.map(({ userId }) => {
return userId;
});
}
public async order(
orderWhereUniqueInput: Prisma.OrderWhereUniqueInput
): Promise<Order | null> {

7
apps/api/src/app/endpoints/asset-profiles/asset-profiles.controller.ts

@ -169,7 +169,12 @@ export class AssetProfilesController {
permissions.deleteAssetProfileSplitOfOwnAssetProfile
});
return this.assetProfilesService.deleteSplit({ id, symbolProfileId });
return this.assetProfilesService.deleteSplit({
dataSource,
id,
symbol,
symbolProfileId
});
}
@HasPermission(permissions.accessAdminControl)

179
apps/api/src/app/endpoints/asset-profiles/asset-profiles.service.spec.ts

@ -0,0 +1,179 @@
import { ActivitiesService } from '@ghostfolio/api/app/activities/activities.service';
import { PortfolioChangedEvent } from '@ghostfolio/api/events/portfolio-changed.event';
import { AssetProfileSplitService } from '@ghostfolio/api/services/asset-profile-split/asset-profile-split.service';
import { DataGatheringService } from '@ghostfolio/api/services/queues/data-gathering/data-gathering.service';
import { NotFoundException } from '@nestjs/common';
import { EventEmitter2 } from '@nestjs/event-emitter';
import { AssetProfileSplit, DataSource } from '@prisma/client';
import { AssetProfilesService } from './asset-profiles.service';
describe('AssetProfilesService', () => {
let assetProfilesService: AssetProfilesService;
let deleteById: jest.Mock;
let emit: jest.Mock;
let finished: jest.Mock;
let gatherSymbol: jest.Mock;
let getUserIdsBySymbolProfileId: jest.Mock;
let upsert: jest.Mock;
beforeEach(() => {
deleteById = jest.fn();
emit = jest.fn();
finished = jest.fn().mockResolvedValue(undefined);
gatherSymbol = jest.fn().mockResolvedValue([{ finished }]);
getUserIdsBySymbolProfileId = jest.fn().mockResolvedValue([]);
upsert = jest.fn();
assetProfilesService = new AssetProfilesService(
{ getUserIdsBySymbolProfileId } as unknown as ActivitiesService,
{
deleteById,
upsert
} as unknown as AssetProfileSplitService,
null,
{ gatherSymbol } as unknown as DataGatheringService,
null,
{ emit } as unknown as EventEmitter2,
null,
null,
null,
null
);
});
describe('createSplit', () => {
it('upserts the split and refreshes the asset profile data', async () => {
const split = {} as AssetProfileSplit;
const data = {
dataSource: DataSource.YAHOO,
date: new Date('2024-06-15T18:30:00.000Z'),
denominator: 1,
numerator: 2,
symbol: 'AAPL',
symbolProfileId: 'profile-id'
};
upsert.mockResolvedValue(split);
const result = await assetProfilesService.createSplit(data);
expect(upsert).toHaveBeenCalledWith({
date: data.date,
denominator: data.denominator,
numerator: data.numerator,
symbolProfileId: data.symbolProfileId
});
expect(gatherSymbol).toHaveBeenCalledWith({
dataSource: data.dataSource,
symbol: data.symbol
});
expect(result).toBe(split);
});
it('invalidates portfolio snapshots for users holding the asset', async () => {
upsert.mockResolvedValue({} as AssetProfileSplit);
getUserIdsBySymbolProfileId.mockResolvedValue(['user-1', 'user-2']);
await assetProfilesService.createSplit({
dataSource: DataSource.YAHOO,
date: new Date('2024-06-15T18:30:00.000Z'),
denominator: 1,
numerator: 2,
symbol: 'AAPL',
symbolProfileId: 'profile-id'
});
await flushPendingPromises();
expect(getUserIdsBySymbolProfileId).toHaveBeenCalledWith('profile-id');
expect(emit.mock.calls.map(([, event]) => event.getUserId())).toEqual([
'user-1',
'user-2'
]);
expect(emit.mock.calls[0][0]).toBe(PortfolioChangedEvent.getName());
});
it('emits the events only once the market data has been gathered', async () => {
let completeJob: () => void;
finished.mockReturnValue(
new Promise<void>((resolve) => {
completeJob = resolve;
})
);
upsert.mockResolvedValue({} as AssetProfileSplit);
getUserIdsBySymbolProfileId.mockResolvedValue(['user-1']);
await assetProfilesService.createSplit({
dataSource: DataSource.YAHOO,
date: new Date('2024-06-15T18:30:00.000Z'),
denominator: 1,
numerator: 2,
symbol: 'AAPL',
symbolProfileId: 'profile-id'
});
await flushPendingPromises();
expect(emit).not.toHaveBeenCalled();
completeJob();
await flushPendingPromises();
expect(emit.mock.calls.map(([, event]) => event.getUserId())).toEqual([
'user-1'
]);
});
});
describe('deleteSplit', () => {
it('throws NotFoundException when the scoped split does not exist', async () => {
deleteById.mockResolvedValue(false);
await expect(
assetProfilesService.deleteSplit({
dataSource: DataSource.YAHOO,
id: 'split-id',
symbol: 'AAPL',
symbolProfileId: 'profile-id'
})
).rejects.toBeInstanceOf(NotFoundException);
await flushPendingPromises();
expect(gatherSymbol).not.toHaveBeenCalled();
expect(emit).not.toHaveBeenCalled();
});
it('deletes an existing split using its profile scope', async () => {
deleteById.mockResolvedValue(true);
getUserIdsBySymbolProfileId.mockResolvedValue(['user-1']);
await expect(
assetProfilesService.deleteSplit({
dataSource: DataSource.YAHOO,
id: 'split-id',
symbol: 'AAPL',
symbolProfileId: 'profile-id'
})
).resolves.toBeUndefined();
await flushPendingPromises();
expect(deleteById).toHaveBeenCalledWith({
id: 'split-id',
symbolProfileId: 'profile-id'
});
expect(emit.mock.calls.map(([, event]) => event.getUserId())).toEqual([
'user-1'
]);
expect(gatherSymbol).toHaveBeenCalledWith({
dataSource: DataSource.YAHOO,
symbol: 'AAPL'
});
});
});
});
function flushPendingPromises() {
return new Promise((resolve) => {
setImmediate(resolve);
});
}

56
apps/api/src/app/endpoints/asset-profiles/asset-profiles.service.ts

@ -1,4 +1,5 @@
import { ActivitiesService } from '@ghostfolio/api/app/activities/activities.service';
import { PortfolioChangedEvent } from '@ghostfolio/api/events/portfolio-changed.event';
import { AssetProfileSplitService } from '@ghostfolio/api/services/asset-profile-split/asset-profile-split.service';
import { BenchmarkService } from '@ghostfolio/api/services/benchmark/benchmark.service';
import { DataProviderService } from '@ghostfolio/api/services/data-provider/data-provider.service';
@ -25,6 +26,7 @@ import {
import { MarketDataPreset } from '@ghostfolio/common/types';
import { Injectable, NotFoundException } from '@nestjs/common';
import { EventEmitter2 } from '@nestjs/event-emitter';
import { AssetClass, AssetSubClass, DataSource, Prisma } from '@prisma/client';
import { groupBy } from 'lodash';
@ -36,6 +38,7 @@ export class AssetProfilesService {
private readonly benchmarkService: BenchmarkService,
private readonly dataGatheringService: DataGatheringService,
private readonly dataProviderService: DataProviderService,
private readonly eventEmitter: EventEmitter2,
private readonly exchangeRateDataService: ExchangeRateDataService,
private readonly marketDataService: MarketDataService,
private readonly prismaService: PrismaService,
@ -62,18 +65,24 @@ export class AssetProfilesService {
symbolProfileId
});
await this.dataGatheringService.gatherSymbol({ dataSource, symbol });
await this.gatherSymbolAndEmitPortfolioChangedEvents({
dataSource,
symbol,
symbolProfileId
});
return assetProfileSplit;
}
public async deleteSplit({
dataSource,
id,
symbol,
symbolProfileId
}: {
id: string;
symbolProfileId: string;
}) {
} & AssetProfileIdentifier) {
const isDeleted = await this.assetProfileSplitService.deleteById({
id,
symbolProfileId
@ -82,6 +91,12 @@ export class AssetProfilesService {
if (!isDeleted) {
throw new NotFoundException();
}
await this.gatherSymbolAndEmitPortfolioChangedEvents({
dataSource,
symbol,
symbolProfileId
});
}
public async getAssetProfile({
@ -425,6 +440,43 @@ export class AssetProfilesService {
return assetProfile;
}
private async emitPortfolioChangedEvents(symbolProfileId: string) {
const userIds =
await this.activitiesService.getUserIdsBySymbolProfileId(symbolProfileId);
for (const userId of userIds) {
this.eventEmitter.emit(
PortfolioChangedEvent.getName(),
new PortfolioChangedEvent({ userId })
);
}
}
/**
* Gathers the market data of the given asset profile and invalidates the
* portfolio snapshots of the affected users as soon as it is available.
* Emitting the events earlier would recompute the snapshots from
* split-adjusted quantities and not yet split-adjusted market prices.
*/
private async gatherSymbolAndEmitPortfolioChangedEvents({
dataSource,
symbol,
symbolProfileId
}: { symbolProfileId: string } & AssetProfileIdentifier) {
const jobs = await this.dataGatheringService.gatherSymbol({
dataSource,
symbol
});
void Promise.allSettled(
jobs.map((job) => {
return job.finished();
})
).then(() => {
return this.emitPortfolioChangedEvents(symbolProfileId);
});
}
private getAssetProfileDataUpdate({
countries,
holdings,

5
apps/api/src/app/endpoints/benchmarks/benchmarks.controller.ts

@ -1,8 +1,8 @@
import { AllowDuringImpersonation } from '@ghostfolio/api/decorators/allow-during-impersonation.decorator';
import { HasPermission } from '@ghostfolio/api/decorators/has-permission.decorator';
import { Impersonation } from '@ghostfolio/api/decorators/impersonation.decorator';
import { RequiresScope } from '@ghostfolio/api/decorators/requires-scope.decorator';
import { HasPermissionGuard } from '@ghostfolio/api/guards/has-permission.guard';
import { ImpersonationGuard } from '@ghostfolio/api/guards/impersonation.guard';
import { TransformDataSourceInRequestInterceptor } from '@ghostfolio/api/interceptors/transform-data-source-in-request/transform-data-source-in-request.interceptor';
import { TransformDataSourceInResponseInterceptor } from '@ghostfolio/api/interceptors/transform-data-source-in-response/transform-data-source-in-response.interceptor';
import { ApiService } from '@ghostfolio/api/services/api/api.service';
@ -14,6 +14,7 @@ import type {
BenchmarkResponse
} from '@ghostfolio/common/interfaces';
import { permissions } from '@ghostfolio/common/permissions';
import { scopes } from '@ghostfolio/common/scopes';
import type { ImpersonationContext } from '@ghostfolio/common/types';
import {
@ -111,7 +112,7 @@ export class BenchmarksController {
}
@Get(':dataSource/:symbol/:startDateString')
@UseGuards(AuthGuard('jwt'), HasPermissionGuard, ImpersonationGuard)
@RequiresScope(scopes.portfolioRead)
@UseInterceptors(TransformDataSourceInRequestInterceptor)
public async getBenchmarkMarketDataForUser(
@Impersonation() { userId, userSettings }: ImpersonationContext,

5
apps/api/src/app/endpoints/watchlist/watchlist.controller.ts

@ -1,12 +1,13 @@
import { HasPermission } from '@ghostfolio/api/decorators/has-permission.decorator';
import { Impersonation } from '@ghostfolio/api/decorators/impersonation.decorator';
import { RequiresScope } from '@ghostfolio/api/decorators/requires-scope.decorator';
import { HasPermissionGuard } from '@ghostfolio/api/guards/has-permission.guard';
import { ImpersonationGuard } from '@ghostfolio/api/guards/impersonation.guard';
import { TransformDataSourceInRequestInterceptor } from '@ghostfolio/api/interceptors/transform-data-source-in-request/transform-data-source-in-request.interceptor';
import { TransformDataSourceInResponseInterceptor } from '@ghostfolio/api/interceptors/transform-data-source-in-response/transform-data-source-in-response.interceptor';
import { CreateWatchlistItemDto } from '@ghostfolio/common/dtos';
import { WatchlistResponse } from '@ghostfolio/common/interfaces';
import { permissions } from '@ghostfolio/common/permissions';
import { scopes } from '@ghostfolio/common/scopes';
import {
ImpersonationContext,
RequestWithUser
@ -82,7 +83,7 @@ export class WatchlistController {
@Get()
@HasPermission(permissions.readWatchlist)
@UseGuards(AuthGuard('jwt'), HasPermissionGuard, ImpersonationGuard)
@RequiresScope(scopes.watchlistRead)
@UseInterceptors(TransformDataSourceInResponseInterceptor)
public async getWatchlistItems(
@Impersonation() { userId }: ImpersonationContext

4
apps/api/src/app/portfolio/calculator/roai/portfolio-calculator-cash.spec.ts

@ -11,6 +11,7 @@ import { CurrentRateService } from '@ghostfolio/api/app/portfolio/current-rate.s
import { CurrentRateServiceMock } from '@ghostfolio/api/app/portfolio/current-rate.service.mock';
import { RedisCacheService } from '@ghostfolio/api/app/redis-cache/redis-cache.service';
import { RedisCacheServiceMock } from '@ghostfolio/api/app/redis-cache/redis-cache.service.mock';
import { AssetProfileSplitService } from '@ghostfolio/api/services/asset-profile-split/asset-profile-split.service';
import { ConfigurationService } from '@ghostfolio/api/services/configuration/configuration.service';
import { DataProviderService } from '@ghostfolio/api/services/data-provider/data-provider.service';
import { ExchangeRateDataService } from '@ghostfolio/api/services/exchange-rate-data/exchange-rate-data.service';
@ -124,6 +125,9 @@ describe('PortfolioCalculator', () => {
activitiesService = new ActivitiesService(
accountBalanceService,
accountService,
{
getSplitsByUserId: jest.fn().mockResolvedValue([])
} as unknown as AssetProfileSplitService,
null,
null,
dataProviderService,

276
apps/api/src/app/portfolio/calculator/roai/portfolio-calculator-stock-split.spec.ts

@ -0,0 +1,276 @@
import {
activityDummyData,
assetProfileDummyData,
userDummyData
} from '@ghostfolio/api/app/portfolio/calculator/portfolio-calculator-test-utils';
import { PortfolioCalculatorFactory } from '@ghostfolio/api/app/portfolio/calculator/portfolio-calculator.factory';
import { CurrentRateService } from '@ghostfolio/api/app/portfolio/current-rate.service';
import { CurrentRateServiceMock } from '@ghostfolio/api/app/portfolio/current-rate.service.mock';
import { RedisCacheService } from '@ghostfolio/api/app/redis-cache/redis-cache.service';
import { RedisCacheServiceMock } from '@ghostfolio/api/app/redis-cache/redis-cache.service.mock';
import { adjustActivityBySplits } from '@ghostfolio/api/services/asset-profile-split/asset-profile-split.helper';
import { ConfigurationService } from '@ghostfolio/api/services/configuration/configuration.service';
import { ExchangeRateDataService } from '@ghostfolio/api/services/exchange-rate-data/exchange-rate-data.service';
import { PortfolioSnapshotService } from '@ghostfolio/api/services/queues/portfolio-snapshot/portfolio-snapshot.service';
import { PortfolioSnapshotServiceMock } from '@ghostfolio/api/services/queues/portfolio-snapshot/portfolio-snapshot.service.mock';
import { parseDate } from '@ghostfolio/common/helper';
import { Activity } from '@ghostfolio/common/interfaces';
import { PerformanceCalculationType } from '@ghostfolio/common/types/performance-calculation-type.type';
import { AssetProfileSplit, DataSource } from '@prisma/client';
import { Big } from 'big.js';
jest.mock('@ghostfolio/api/app/portfolio/current-rate.service', () => {
return {
CurrentRateService: jest.fn().mockImplementation(() => {
return CurrentRateServiceMock;
})
};
});
jest.mock(
'@ghostfolio/api/services/queues/portfolio-snapshot/portfolio-snapshot.service',
() => {
return {
PortfolioSnapshotService: jest.fn().mockImplementation(() => {
return PortfolioSnapshotServiceMock;
})
};
}
);
jest.mock('@ghostfolio/api/app/redis-cache/redis-cache.service', () => {
return {
RedisCacheService: jest.fn().mockImplementation(() => {
return RedisCacheServiceMock;
})
};
});
describe('RoaiPortfolioCalculator stock splits', () => {
let configurationService: ConfigurationService;
let currentRateService: CurrentRateService;
let exchangeRateDataService: ExchangeRateDataService;
let portfolioCalculatorFactory: PortfolioCalculatorFactory;
let portfolioSnapshotService: PortfolioSnapshotService;
let redisCacheService: RedisCacheService;
beforeEach(() => {
PortfolioSnapshotServiceMock.reset();
RedisCacheServiceMock.reset();
configurationService = new ConfigurationService();
currentRateService = new CurrentRateService(null, null, null, null);
exchangeRateDataService = new ExchangeRateDataService(
null,
null,
null,
null
);
portfolioSnapshotService = new PortfolioSnapshotService(null, null);
redisCacheService = new RedisCacheService(null, null);
portfolioCalculatorFactory = new PortfolioCalculatorFactory(
configurationService,
currentRateService,
exchangeRateDataService,
portfolioSnapshotService,
redisCacheService
);
});
it('doubles the position and halves the average price for a 2:1 split', () => {
const activity = adjustActivityBySplits(
createActivity({ unitPrice: 100 }),
[createSplit({ denominator: 1, numerator: 2 })]
);
const position = getLastPosition(portfolioCalculatorFactory, [activity]);
expect(position).toMatchObject({
averagePrice: new Big(50),
investment: new Big(1000),
quantity: new Big(20)
});
});
it('applies the inverse quantity and price changes for a reverse split', () => {
const activity = adjustActivityBySplits(
createActivity({ unitPrice: 100 }),
[createSplit({ denominator: 10, numerator: 1 })]
);
const position = getLastPosition(portfolioCalculatorFactory, [activity]);
expect(position).toMatchObject({
averagePrice: new Big(1000),
investment: new Big(1000),
quantity: new Big(1)
});
});
it('uses adjusted quantities when selling after a split', () => {
const buy = adjustActivityBySplits(
createActivity({ date: '2020-01-01', unitPrice: 100 }),
[createSplit({ denominator: 1, numerator: 2 })]
);
const sell = createActivity({
date: '2021-01-01',
quantity: 5,
type: 'SELL',
unitPrice: 60
});
const position = getLastPosition(portfolioCalculatorFactory, [buy, sell]);
expect(position).toMatchObject({
averagePrice: new Big(50),
investment: new Big(750),
quantity: new Big(15)
});
});
it('applies multiple splits while preserving fractional precision', () => {
const activity = adjustActivityBySplits(
createActivity({ unitPrice: 100 }),
[
createSplit({ denominator: 1, numerator: 2 }),
createSplit({ denominator: 3, numerator: 1, date: '2022-01-01' })
]
);
const position = getLastPosition(portfolioCalculatorFactory, [activity]);
expect(position.averagePrice).toEqual(new Big(150));
expect(position.quantity.toFixed(15)).toBe(new Big(20).div(3).toFixed(15));
expect(position.investment.toNumber()).toBeCloseTo(1000, 12);
});
it('resets quantity and investment when the adjusted position is closed', () => {
const buy = adjustActivityBySplits(
createActivity({ date: '2020-01-01', unitPrice: 100 }),
[createSplit({ denominator: 1, numerator: 2 })]
);
const sell = createActivity({
date: '2021-01-01',
quantity: 20,
type: 'SELL',
unitPrice: 60
});
const position = getLastPosition(portfolioCalculatorFactory, [buy, sell]);
expect(position.quantity).toEqual(new Big(0));
expect(position.investment).toEqual(new Big(0));
});
it('preserves existing behavior when no splits exist', () => {
const position = getLastPosition(portfolioCalculatorFactory, [
createActivity({ unitPrice: 100 })
]);
expect(position).toMatchObject({
averagePrice: new Big(100),
investment: new Big(1000),
quantity: new Big(10)
});
});
it('uses provider market data without adjusting it a second time', async () => {
jest.useFakeTimers().setSystemTime(parseDate('2023-07-10').getTime());
const activity = adjustActivityBySplits(
createActivity({ date: '2023-07-09', unitPrice: 674.44 }),
[
createSplit({
date: '2023-07-10',
denominator: 1,
numerator: 2
})
]
);
const calculator = portfolioCalculatorFactory.createCalculator({
activities: [activity],
calculationType: PerformanceCalculationType.ROAI,
currency: 'USD',
userId: userDummyData.id
});
const snapshot = await calculator.computeSnapshot();
const [position] = snapshot.positions;
expect(position).toMatchObject({
investment: new Big(6744.4),
marketPrice: 331.83,
quantity: new Big(20),
valueInBaseCurrency: new Big(6636.6)
});
});
});
function getLastPosition(
portfolioCalculatorFactory: PortfolioCalculatorFactory,
activities: Activity[]
) {
const calculator = portfolioCalculatorFactory.createCalculator({
activities,
calculationType: PerformanceCalculationType.ROAI,
currency: 'USD',
userId: userDummyData.id
});
return calculator.getTransactionPoints().at(-1).items[0];
}
function createActivity({
date = '2020-01-01',
quantity = 10,
type = 'BUY',
unitPrice = 100
}: {
date?: string;
quantity?: number;
type?: Activity['type'];
unitPrice?: number;
}): Activity {
return {
...activityDummyData,
assetProfile: {
...assetProfileDummyData,
currency: 'USD',
dataSource: DataSource.YAHOO,
name: 'Microsoft Inc.',
symbol: 'MSFT'
},
date: parseDate(date),
feeInAssetProfileCurrency: 0,
feeInBaseCurrency: 0,
quantity,
type,
unitPrice,
unitPriceInAssetProfileCurrency: unitPrice,
value: quantity * unitPrice,
valueInBaseCurrency: quantity * unitPrice
} as Activity;
}
function createSplit({
date = '2021-01-01',
denominator,
numerator
}: {
date?: string;
denominator: number;
numerator: number;
}): AssetProfileSplit {
const splitDate = parseDate(date);
return {
denominator,
numerator,
createdAt: splitDate,
date: splitDate,
id: `${date}-${numerator}-${denominator}`,
symbolProfileId: 'msft-profile',
updatedAt: splitDate
};
}

70
apps/api/src/app/portfolio/portfolio.controller.ts

@ -1,8 +1,8 @@
import { ActivitiesService } from '@ghostfolio/api/app/activities/activities.service';
import { HasPermission } from '@ghostfolio/api/decorators/has-permission.decorator';
import { Impersonation } from '@ghostfolio/api/decorators/impersonation.decorator';
import { RequiresScope } from '@ghostfolio/api/decorators/requires-scope.decorator';
import { HasPermissionGuard } from '@ghostfolio/api/guards/has-permission.guard';
import { ImpersonationGuard } from '@ghostfolio/api/guards/impersonation.guard';
import {
hasNotDefinedValuesInObject,
nullifyValuesInObject
@ -25,11 +25,8 @@ import {
PortfolioPerformanceResponse,
PortfolioReportResponse
} from '@ghostfolio/common/interfaces';
import {
hasReadRestrictedAccessPermission,
isRestrictedView,
permissions
} from '@ghostfolio/common/permissions';
import { isRestrictedView, permissions } from '@ghostfolio/common/permissions';
import { hasScope, scopes } from '@ghostfolio/common/scopes';
import type {
ImpersonationContext,
RequestWithUser
@ -73,12 +70,13 @@ export class PortfolioController {
) {}
@Get('details')
@UseGuards(AuthGuard('jwt'), HasPermissionGuard, ImpersonationGuard)
@RequiresScope(scopes.portfolioRead)
@UseInterceptors(RedactValuesInResponseInterceptor)
@UseInterceptors(TransformDataSourceInRequestInterceptor)
@UseInterceptors(TransformDataSourceInResponseInterceptor)
public async getDetails(
@Impersonation() { accessId, userId }: ImpersonationContext,
@Impersonation()
{ scopes: impersonationScopes, userId }: ImpersonationContext,
@Query()
{
accounts: filterByAccounts,
@ -130,10 +128,7 @@ export class PortfolioController {
let portfolioSummary = summary;
if (
hasReadRestrictedAccessPermission({
accesses: this.request.user?.accessesGet,
impersonationId: accessId
}) ||
!hasScope(impersonationScopes, scopes.portfolioReadValues) ||
isRestrictedView(this.request.user)
) {
const totalInvestment = Object.values(holdings)
@ -174,10 +169,7 @@ export class PortfolioController {
if (
hasDetails === false ||
hasReadRestrictedAccessPermission({
accesses: this.request.user?.accessesGet,
impersonationId: accessId
}) ||
!hasScope(impersonationScopes, scopes.portfolioReadValues) ||
isRestrictedView(this.request.user)
) {
Object.values(markets ?? {}).forEach((market) => {
@ -319,10 +311,11 @@ export class PortfolioController {
}
@Get('dividends')
@UseGuards(AuthGuard('jwt'), HasPermissionGuard, ImpersonationGuard)
@RequiresScope(scopes.portfolioRead)
@UseInterceptors(TransformDataSourceInRequestInterceptor)
public async getDividends(
@Impersonation() { accessId, userId, userSettings }: ImpersonationContext,
@Impersonation()
{ scopes: impersonationScopes, userId, userSettings }: ImpersonationContext,
@Query()
{
accounts,
@ -364,10 +357,7 @@ export class PortfolioController {
});
if (
hasReadRestrictedAccessPermission({
accesses: this.request.user?.accessesGet,
impersonationId: accessId
}) ||
!hasScope(impersonationScopes, scopes.portfolioReadValues) ||
isRestrictedView(this.request.user)
) {
const maxDividend = dividends.reduce(
@ -397,7 +387,7 @@ export class PortfolioController {
@UseInterceptors(RedactValuesInResponseInterceptor)
@UseInterceptors(TransformDataSourceInRequestInterceptor)
@UseInterceptors(TransformDataSourceInResponseInterceptor)
@UseGuards(AuthGuard('jwt'), HasPermissionGuard, ImpersonationGuard)
@RequiresScope(scopes.portfolioRead)
public async getHolding(
@Impersonation() { userId }: ImpersonationContext,
@Param('dataSource') dataSource: DataSource,
@ -420,7 +410,7 @@ export class PortfolioController {
}
@Get('holdings')
@UseGuards(AuthGuard('jwt'), HasPermissionGuard, ImpersonationGuard)
@RequiresScope(scopes.portfolioRead)
@UseInterceptors(RedactValuesInResponseInterceptor)
@UseInterceptors(TransformDataSourceInRequestInterceptor)
@UseInterceptors(TransformDataSourceInResponseInterceptor)
@ -458,10 +448,11 @@ export class PortfolioController {
}
@Get('investments')
@UseGuards(AuthGuard('jwt'), HasPermissionGuard, ImpersonationGuard)
@RequiresScope(scopes.portfolioRead)
@UseInterceptors(TransformDataSourceInRequestInterceptor)
public async getInvestments(
@Impersonation() { accessId, userId }: ImpersonationContext,
@Impersonation()
{ scopes: impersonationScopes, userId }: ImpersonationContext,
@Query()
{
accounts,
@ -490,10 +481,7 @@ export class PortfolioController {
});
if (
hasReadRestrictedAccessPermission({
accesses: this.request.user?.accessesGet,
impersonationId: accessId
}) ||
!hasScope(impersonationScopes, scopes.portfolioReadValues) ||
isRestrictedView(this.request.user)
) {
const maxInvestment = investments.reduce(
@ -532,13 +520,14 @@ export class PortfolioController {
}
@Get('performance')
@UseGuards(AuthGuard('jwt'), HasPermissionGuard, ImpersonationGuard)
@RequiresScope(scopes.portfolioRead)
@UseInterceptors(PerformanceLoggingInterceptor)
@UseInterceptors(TransformDataSourceInRequestInterceptor)
@UseInterceptors(TransformDataSourceInResponseInterceptor)
@Version('2')
public async getPerformanceV2(
@Impersonation() { accessId, userId }: ImpersonationContext,
@Impersonation()
{ scopes: impersonationScopes, userId }: ImpersonationContext,
@Query()
{
accounts,
@ -566,10 +555,7 @@ export class PortfolioController {
});
if (
hasReadRestrictedAccessPermission({
accesses: this.request.user?.accessesGet,
impersonationId: accessId
}) ||
!hasScope(impersonationScopes, scopes.portfolioReadValues) ||
isRestrictedView(this.request.user) ||
this.request.user.settings.settings.viewMode === 'ZEN'
) {
@ -645,17 +631,15 @@ export class PortfolioController {
}
@Get('report')
@UseGuards(AuthGuard('jwt'), HasPermissionGuard, ImpersonationGuard)
@RequiresScope(scopes.portfolioRead)
public async getReport(
@Impersonation() { accessId, userId }: ImpersonationContext
@Impersonation()
{ scopes: impersonationScopes, userId }: ImpersonationContext
): Promise<PortfolioReportResponse> {
const report = await this.portfolioService.getReport({ userId });
if (
hasReadRestrictedAccessPermission({
accesses: this.request.user?.accessesGet,
impersonationId: accessId
}) ||
!hasScope(impersonationScopes, scopes.portfolioReadValues) ||
isRestrictedView(this.request.user) ||
(this.configurationService.get('ENABLE_FEATURE_SUBSCRIPTION') &&
this.request.user.subscription?.type === SubscriptionType.Basic)
@ -676,7 +660,7 @@ export class PortfolioController {
@HasPermission(permissions.updateActivity)
@Put('holding/:dataSource/:symbol/tags')
@UseInterceptors(TransformDataSourceInRequestInterceptor)
@UseGuards(AuthGuard('jwt'), HasPermissionGuard, ImpersonationGuard)
@UseGuards(AuthGuard('jwt'), HasPermissionGuard)
public async updateHoldingTags(
@Body() data: UpdateHoldingTagsDto,
@Param('dataSource') dataSource: DataSource,

2
apps/api/src/app/portfolio/portfolio.service.spec.ts

@ -66,6 +66,7 @@ describe('PortfolioService', () => {
accountService,
null,
null,
null,
dataProviderService,
null,
exchangeRateDataService,
@ -245,7 +246,6 @@ describe('PortfolioService', () => {
.mockResolvedValue([]);
jest.spyOn(userService, 'user').mockResolvedValue({
accessesGet: [],
accounts: [],
activityCount: 0,
dataProviderGhostfolioDailyRequests: 0,

3
apps/api/src/app/user/user.controller.ts

@ -129,9 +129,10 @@ export class UserController {
@UseInterceptors(TransformDataSourceInResponseInterceptor)
public async getUser(
@Headers('accept-language') acceptLanguage: string,
@Impersonation() { isActive, userId }: ImpersonationContext
@Impersonation() { isActive, scopes, userId }: ImpersonationContext
): Promise<User> {
return this.userService.getUser({
scopes,
impersonationUserId: isActive ? userId : undefined,
locale: acceptLanguage?.split(',')?.[0],
user: this.request.user

10
apps/api/src/app/user/user.service.ts

@ -53,6 +53,7 @@ import {
hasRole,
permissions
} from '@ghostfolio/common/permissions';
import { getScopesOfAccess } from '@ghostfolio/common/scopes';
import { UserWithSettings } from '@ghostfolio/common/types';
import { PerformanceCalculationType } from '@ghostfolio/common/types/performance-calculation-type.type';
@ -115,10 +116,12 @@ export class UserService {
public async getUser({
impersonationUserId,
locale = DEFAULT_LOCALE,
scopes,
user
}: {
impersonationUserId: string;
locale?: string;
scopes: string[];
user: UserWithSettings;
}): Promise<IUser> {
const { id, permissions, settings, subscription } = user;
@ -206,6 +209,7 @@ export class UserService {
id,
permissions,
referralPartners,
scopes,
subscription,
systemMessage,
tags,
@ -213,7 +217,8 @@ export class UserService {
return {
alias: accessItem.alias,
id: accessItem.id,
permissions: accessItem.permissions
permissions: accessItem.permissions,
scopes: getScopesOfAccess(accessItem)
};
}),
accounts: accounts.sort((a, b) => {
@ -282,7 +287,6 @@ export class UserService {
activities: true
}
},
accessesGet: true,
accounts: {
include: { platform: true }
},
@ -299,7 +303,6 @@ export class UserService {
const {
_count,
accessesGet,
accessToken,
accounts,
analytics,
@ -317,7 +320,6 @@ export class UserService {
const activitiesCount = _count?.activities ?? 0;
const user: UserWithSettings = {
accessesGet,
accessToken,
accounts,
authChallenge,

2
apps/api/src/decorators/impersonation.decorator.ts

@ -1,3 +1,4 @@
import { getScopesOfOwnAccess } from '@ghostfolio/common/scopes';
import type {
ImpersonationContext,
RequestWithUser
@ -18,6 +19,7 @@ export const Impersonation = createParamDecorator(
return (
impersonation ?? {
isActive: false,
scopes: getScopesOfOwnAccess(),
userId: user?.id,
userSettings: user?.settings?.settings ?? {}
}

41
apps/api/src/decorators/requires-scope.decorator.spec.ts

@ -0,0 +1,41 @@
import { HasPermissionGuard } from '@ghostfolio/api/guards/has-permission.guard';
import { ImpersonationGuard } from '@ghostfolio/api/guards/impersonation.guard';
import { ScopeGuard } from '@ghostfolio/api/guards/scope.guard';
import { scopes } from '@ghostfolio/common/scopes';
import { GUARDS_METADATA } from '@nestjs/common/constants';
import { AuthGuard } from '@nestjs/passport';
import { REQUIRES_SCOPE_KEY, RequiresScope } from './requires-scope.decorator';
class TestController {
@RequiresScope(scopes.portfolioRead)
public getPortfolio() {
return null;
}
}
describe('Requires scope', () => {
it('Sets the required scopes', () => {
expect(
Reflect.getMetadata(
REQUIRES_SCOPE_KEY,
TestController.prototype.getPortfolio
)
).toEqual([scopes.portfolioRead]);
});
it('Applies the guards in the required order', () => {
expect(
Reflect.getMetadata(
GUARDS_METADATA,
TestController.prototype.getPortfolio
)
).toEqual([
AuthGuard('jwt'),
HasPermissionGuard,
ImpersonationGuard,
ScopeGuard
]);
});
});

26
apps/api/src/decorators/requires-scope.decorator.ts

@ -0,0 +1,26 @@
import { HasPermissionGuard } from '@ghostfolio/api/guards/has-permission.guard';
import { ImpersonationGuard } from '@ghostfolio/api/guards/impersonation.guard';
import { ScopeGuard } from '@ghostfolio/api/guards/scope.guard';
import { Scope } from '@ghostfolio/common/scopes';
import { applyDecorators, SetMetadata, UseGuards } from '@nestjs/common';
import { AuthGuard } from '@nestjs/passport';
export const REQUIRES_SCOPE_KEY = 'requires_scope';
/**
* Marks a route which requires the given scopes and applies the guards which
* resolve the impersonation context and evaluate it, hence the ScopeGuard
* cannot be applied without the ImpersonationGuard preceding it
*/
export function RequiresScope(...requiredScopes: Scope[]) {
return applyDecorators(
SetMetadata(REQUIRES_SCOPE_KEY, requiredScopes),
UseGuards(
AuthGuard('jwt'),
HasPermissionGuard,
ImpersonationGuard,
ScopeGuard
)
);
}

50
apps/api/src/guards/scope.guard.ts

@ -0,0 +1,50 @@
import { REQUIRES_SCOPE_KEY } from '@ghostfolio/api/decorators/requires-scope.decorator';
import { hasScope, Scope } from '@ghostfolio/common/scopes';
import type { RequestWithUser } from '@ghostfolio/common/types';
import {
CanActivate,
ExecutionContext,
HttpException,
Injectable
} from '@nestjs/common';
import { Reflector } from '@nestjs/core';
import { StatusCodes, getReasonPhrase } from 'http-status-codes';
/**
* Denies a request whose impersonation context does not cover the scopes
* required by the route. It has to be applied after the ImpersonationGuard,
* which resolves the context, hence the RequiresScope decorator applies both.
*/
@Injectable()
export class ScopeGuard implements CanActivate {
public constructor(private readonly reflector: Reflector) {}
public canActivate(context: ExecutionContext): boolean {
const requiredScopes = this.reflector.getAllAndOverride<Scope[]>(
REQUIRES_SCOPE_KEY,
[context.getHandler(), context.getClass()]
);
if (!requiredScopes?.length) {
return true;
}
const { impersonation } = context
.switchToHttp()
.getRequest<RequestWithUser>();
const hasRequiredScopes = requiredScopes.every((scope) => {
return hasScope(impersonation?.scopes, scope);
});
if (!hasRequiredScopes) {
throw new HttpException(
getReasonPhrase(StatusCodes.FORBIDDEN),
StatusCodes.FORBIDDEN
);
}
return true;
}
}

29
apps/api/src/interceptors/redact-values-in-response/redact-values-in-response.interceptor.ts

@ -1,13 +1,8 @@
import { redactPaths } from '@ghostfolio/api/helper/object.helper';
import {
DEFAULT_REDACTED_PATHS,
HEADER_KEY_IMPERSONATION
} from '@ghostfolio/common/config';
import {
hasReadRestrictedAccessPermission,
isRestrictedView
} from '@ghostfolio/common/permissions';
import { UserWithSettings } from '@ghostfolio/common/types';
import { DEFAULT_REDACTED_PATHS } from '@ghostfolio/common/config';
import { isRestrictedView } from '@ghostfolio/common/permissions';
import { hasScope, scopes } from '@ghostfolio/common/scopes';
import type { RequestWithUser } from '@ghostfolio/common/types';
import {
CallHandler,
@ -29,17 +24,15 @@ export class RedactValuesInResponseInterceptor<T> implements NestInterceptor<
): Observable<any> {
return next.handle().pipe(
map((data: any) => {
const { headers, user }: { headers: Headers; user: UserWithSettings } =
context.switchToHttp().getRequest();
const impersonationId =
headers?.[HEADER_KEY_IMPERSONATION.toLowerCase()];
const { impersonation, user } = context
.switchToHttp()
.getRequest<RequestWithUser>();
// A missing impersonation context originates from a public request or
// from a route without the ImpersonationGuard, hence the monetary
// values are redacted to never expose them unintentionally
if (
hasReadRestrictedAccessPermission({
impersonationId,
accesses: user?.accessesGet
}) ||
!hasScope(impersonation?.scopes, scopes.portfolioReadValues) ||
isRestrictedView(user)
) {
data = redactPaths({

114
apps/api/src/services/asset-profile-split/asset-profile-split.helper.spec.ts

@ -0,0 +1,114 @@
import {
activityDummyData,
assetProfileDummyData
} from '@ghostfolio/api/app/portfolio/calculator/portfolio-calculator-test-utils';
import * as commonHelper from '@ghostfolio/common/helper';
import { parseDate } from '@ghostfolio/common/helper';
import { Activity } from '@ghostfolio/common/interfaces';
import { AssetProfileSplit, DataSource } from '@prisma/client';
import { Big } from 'big.js';
import { adjustActivityBySplits } from './asset-profile-split.helper';
describe('adjustActivityBySplits', () => {
it('adjusts quantity and prices using the cumulative split factor', () => {
const activity = createActivity('2020-01-01');
const splits = [
createSplit('2021-01-01', 2, 1),
createSplit('2022-01-01', 1, 3)
];
const adjustedActivity = adjustActivityBySplits(activity, splits);
expect(adjustedActivity).not.toBe(activity);
expect(adjustedActivity).toMatchObject({
quantity: 20 / 3,
unitPrice: 150,
unitPriceInAssetProfileCurrency: 150,
value: 1000,
valueInBaseCurrency: 1000
});
expect(new Big(adjustedActivity.quantity).toFixed(15)).toBe(
new Big(20).div(3).toFixed(15)
);
expect(activity).toMatchObject({
quantity: 10,
unitPrice: 100,
unitPriceInAssetProfileCurrency: 100
});
});
it('only adjusts activities before the split calendar date', () => {
const split = createSplit('2024-06-15T00:00:00Z', 2, 1);
const activityOnSplitDate = createActivity('2024-06-15T18:00:00Z');
const activityAfterSplit = createActivity('2024-06-16T00:00:00Z');
const adjustedActivityOnSplitDate = adjustActivityBySplits(
activityOnSplitDate,
[split]
);
const adjustedActivityAfterSplit = adjustActivityBySplits(
activityAfterSplit,
[split]
);
expect(adjustedActivityOnSplitDate).toEqual(activityOnSplitDate);
expect(adjustedActivityOnSplitDate).toBe(activityOnSplitDate);
expect(adjustedActivityAfterSplit).toEqual(activityAfterSplit);
expect(adjustedActivityAfterSplit).toBe(activityAfterSplit);
});
it('compares stored UTC split dates without normalizing them locally', () => {
const resetHoursSpy = jest
.spyOn(commonHelper, 'resetHours')
.mockReturnValue(new Date('2024-06-14T00:00:00Z'));
try {
const activity = createActivity('2024-06-14T12:00:00Z');
const split = createSplit('2024-06-15T00:00:00Z', 2, 1);
const adjustedActivity = adjustActivityBySplits(activity, [split]);
expect(adjustedActivity.quantity).toBe(20);
} finally {
resetHoursSpy.mockRestore();
}
});
});
function createActivity(date: string): Activity {
return {
...activityDummyData,
assetProfile: {
...assetProfileDummyData,
dataSource: DataSource.YAHOO,
symbol: 'AAPL'
},
date: parseDate(date),
quantity: 10,
type: 'BUY',
unitPrice: 100,
unitPriceInAssetProfileCurrency: 100,
value: 1000,
valueInBaseCurrency: 1000
} as Activity;
}
function createSplit(
date: string,
numerator: number,
denominator: number
): AssetProfileSplit {
const splitDate = new Date(date);
return {
denominator,
numerator,
createdAt: splitDate,
date: splitDate,
id: `${date}-${numerator}-${denominator}`,
symbolProfileId: 'aapl-profile',
updatedAt: splitDate
};
}

58
apps/api/src/services/asset-profile-split/asset-profile-split.helper.ts

@ -0,0 +1,58 @@
import { INVESTMENT_ACTIVITY_TYPES } from '@ghostfolio/common/config';
import { resetHours } from '@ghostfolio/common/helper';
import { Activity } from '@ghostfolio/common/interfaces';
import { AssetProfileSplit } from '@prisma/client';
import { Big } from 'big.js';
import { isBefore } from 'date-fns';
export function adjustActivityBySplits(
activity: Activity,
splits: AssetProfileSplit[]
): Activity {
if (!INVESTMENT_ACTIVITY_TYPES.includes(activity.type)) {
return activity;
}
const activityDate = resetHours(activity.date);
// Accumulate both parts of the ratio and divide only once, so that the
// cumulative split factor of consecutive splits stays exact
let denominator = new Big(1);
let numerator = new Big(1);
for (const split of splits) {
// Skip malformed splits to not break the portfolio calculation of every
// user holding this asset profile
if (split.denominator <= 0 || split.numerator <= 0) {
continue;
}
if (isBefore(activityDate, split.date)) {
denominator = denominator.mul(split.denominator);
numerator = numerator.mul(split.numerator);
}
}
if (numerator.eq(denominator)) {
return activity;
}
return {
...activity,
quantity: new Big(activity.quantity)
.mul(numerator)
.div(denominator)
.toNumber(),
unitPrice: new Big(activity.unitPrice)
.mul(denominator)
.div(numerator)
.toNumber(),
unitPriceInAssetProfileCurrency: new Big(
activity.unitPriceInAssetProfileCurrency
)
.mul(denominator)
.div(numerator)
.toNumber()
};
}

161
apps/api/src/services/asset-profile-split/asset-profile-split.service.spec.ts

@ -0,0 +1,161 @@
import { PrismaService } from '@ghostfolio/api/services/prisma/prisma.service';
import { AssetProfileSplit, DataSource } from '@prisma/client';
import { AssetProfileSplitService } from './asset-profile-split.service';
describe('AssetProfileSplitService', () => {
let assetProfileSplitService: AssetProfileSplitService;
let deleteMany: jest.Mock;
let findMany: jest.Mock;
let upsert: jest.Mock;
beforeEach(() => {
deleteMany = jest.fn();
findMany = jest.fn();
upsert = jest.fn();
assetProfileSplitService = new AssetProfileSplitService({
assetProfileSplit: { deleteMany, findMany, upsert }
} as unknown as PrismaService);
});
describe('deleteById', () => {
it('scopes deletion by split and asset profile identifiers', async () => {
deleteMany.mockResolvedValue({ count: 1 });
const result = await assetProfileSplitService.deleteById({
id: 'split-id',
symbolProfileId: 'profile-id'
});
expect(result).toBe(true);
expect(deleteMany).toHaveBeenCalledWith({
where: {
id: 'split-id',
symbolProfileId: 'profile-id'
}
});
});
it('returns false when the split belongs to another asset profile', async () => {
deleteMany.mockResolvedValue({ count: 0 });
const result = await assetProfileSplitService.deleteById({
id: 'split-id',
symbolProfileId: 'other-profile-id'
});
expect(result).toBe(false);
expect(deleteMany).toHaveBeenCalledWith({
where: {
id: 'split-id',
symbolProfileId: 'other-profile-id'
}
});
});
});
describe('getSplitsByUserId', () => {
it('fetches the splits of the asset profiles held by the user with one ordered query', async () => {
const splits = [
createStoredSplit('2020-08-31'),
createStoredSplit('2021-09-16')
];
findMany.mockResolvedValue(splits);
const result = await assetProfileSplitService.getSplitsByUserId({
userId: 'user-id'
});
expect(result).toBe(splits);
expect(findMany).toHaveBeenCalledTimes(1);
expect(findMany).toHaveBeenCalledWith({
orderBy: [{ date: 'asc' }],
where: {
symbolProfile: {
activities: {
some: {
userId: 'user-id'
}
}
}
}
});
});
});
describe('upsert', () => {
it('normalizes the split date before persisting it', async () => {
const date = new Date('2024-06-15T18:30:00.000Z');
const normalizedDate = new Date('2024-06-15T00:00:00.000Z');
await assetProfileSplitService.upsert({
date,
denominator: 1,
numerator: 2,
symbolProfileId: 'profile-id'
});
expect(upsert).toHaveBeenCalledWith({
create: {
date: normalizedDate,
denominator: 1,
numerator: 2,
symbolProfileId: 'profile-id'
},
update: {
denominator: 1,
numerator: 2
},
where: {
symbolProfileId_date: {
date: normalizedDate,
symbolProfileId: 'profile-id'
}
}
});
});
});
describe('getSplits', () => {
it('filters by asset profile and orders splits by date ascending', async () => {
const splits = [
createStoredSplit('2020-01-01'),
createStoredSplit('2021-01-01')
];
findMany.mockResolvedValue(splits);
const result = await assetProfileSplitService.getSplits({
dataSource: DataSource.YAHOO,
symbol: 'AAPL'
});
expect(result).toBe(splits);
expect(findMany).toHaveBeenCalledWith({
orderBy: [{ date: 'asc' }],
where: {
symbolProfile: {
dataSource: DataSource.YAHOO,
symbol: 'AAPL'
}
}
});
});
});
});
function createStoredSplit(date: string): AssetProfileSplit {
const splitDate = new Date(date);
return {
createdAt: splitDate,
date: splitDate,
denominator: 1,
id: `${date}-split`,
numerator: 2,
symbolProfileId: 'aapl-profile',
updatedAt: splitDate
};
}

27
apps/api/src/services/asset-profile-split/asset-profile-split.service.ts

@ -52,6 +52,33 @@ export class AssetProfileSplitService {
});
}
/**
* Returns the splits of all asset profiles the given user has activities
* for, in ascending order by date
*/
public async getSplitsByUserId({
userId
}: {
userId: string;
}): Promise<AssetProfileSplit[]> {
return this.prismaService.assetProfileSplit.findMany({
orderBy: [
{
date: 'asc'
}
],
where: {
symbolProfile: {
activities: {
some: {
userId
}
}
}
}
});
}
public async upsert({
date,
denominator,

32
apps/api/src/services/impersonation/impersonation.service.ts

@ -2,12 +2,18 @@ import { PrismaService } from '@ghostfolio/api/services/prisma/prisma.service';
import { DEFAULT_CURRENCY } from '@ghostfolio/common/config';
import { UserSettings } from '@ghostfolio/common/interfaces';
import { hasPermission, permissions } from '@ghostfolio/common/permissions';
import {
getScopesOfAccess,
getScopesOfOwnAccess,
getScopesOfUnrestrictedImpersonation
} from '@ghostfolio/common/scopes';
import type {
ImpersonationContext,
UserWithSettings
} from '@ghostfolio/common/types';
import { Injectable } from '@nestjs/common';
import { Access } from '@prisma/client';
@Injectable()
export class ImpersonationService {
@ -20,14 +26,13 @@ export class ImpersonationService {
impersonationId?: string;
user?: UserWithSettings;
}): Promise<ImpersonationContext> {
const impersonatedUserId = await this.validateImpersonationId({
impersonationId,
user
});
const { access, userId: impersonatedUserId } =
await this.validateImpersonation({ impersonationId, user });
if (!impersonatedUserId) {
return {
isActive: false,
scopes: getScopesOfOwnAccess(),
userId: user?.id,
userSettings: user?.settings?.settings ?? {}
};
@ -40,6 +45,11 @@ export class ImpersonationService {
return {
accessId: impersonationId,
isActive: true,
// An access which has not been granted explicitly originates from the
// permission to impersonate all users
scopes: access
? getScopesOfAccess(access)
: getScopesOfUnrestrictedImpersonation(),
userId: impersonatedUserId,
userSettings: {
...((settings?.settings ?? {}) as UserSettings),
@ -49,15 +59,15 @@ export class ImpersonationService {
};
}
public async validateImpersonationId({
private async validateImpersonation({
impersonationId,
user
}: {
impersonationId?: string;
user?: UserWithSettings;
}) {
}): Promise<{ access?: Access; userId: string | null }> {
if (!impersonationId) {
return null;
return { userId: null };
}
if (user) {
@ -69,7 +79,7 @@ export class ImpersonationService {
});
if (accessObject?.userId) {
return accessObject.userId;
return { access: accessObject, userId: accessObject.userId };
} else if (
hasPermission(user.permissions, permissions.impersonateAllUsers)
) {
@ -79,7 +89,7 @@ export class ImpersonationService {
where: { id: impersonationId }
});
return impersonatedUser?.id ?? null;
return { userId: impersonatedUser?.id ?? null };
}
} else {
// Public access
@ -91,10 +101,10 @@ export class ImpersonationService {
});
if (accessObject?.userId) {
return accessObject.userId;
return { access: accessObject, userId: accessObject.userId };
}
}
return null;
return { userId: null };
}
}

8
apps/api/src/services/queues/data-gathering/data-gathering.service.ts

@ -29,7 +29,7 @@ import {
import { InjectQueue } from '@nestjs/bull';
import { Inject, Injectable, Logger } from '@nestjs/common';
import { Prisma } from '@prisma/client';
import { JobOptions, Queue } from 'bull';
import { Job, JobOptions, Queue } from 'bull';
import { format, min, subDays, subMilliseconds, subYears } from 'date-fns';
import { isEmpty } from 'lodash';
import ms, { StringValue } from 'ms';
@ -285,7 +285,7 @@ export class DataGatheringService {
date: date ?? item.date
}));
await this.gatherSymbols({
return this.gatherSymbols({
dataGatheringItems,
force: true,
priority: DATA_GATHERING_QUEUE_PRIORITY_HIGH
@ -336,8 +336,8 @@ export class DataGatheringService {
dataGatheringItems: DataGatheringItem[];
force?: boolean;
priority: number;
}) {
await this.addJobsToQueue(
}): Promise<Job[]> {
return this.addJobsToQueue(
dataGatheringItems.map(({ dataSource, date, symbol }) => {
return {
data: {

4
apps/client/src/app/components/access-table/access-table.component.html

@ -18,10 +18,10 @@
<th *matHeaderCellDef class="px-1" i18n mat-header-cell>Permission</th>
<td *matCellDef="let element" class="px-1 text-nowrap" mat-cell>
<div class="align-items-center d-flex">
@if (element.permissions.includes('READ')) {
@if (hasScopeToReadValues(element)) {
<ion-icon class="mr-1" name="lock-open-outline" />
<ng-container i18n>View</ng-container>
} @else if (element.permissions.includes('READ_RESTRICTED')) {
} @else {
<ion-icon class="mr-1" name="lock-closed-outline" />
<ng-container i18n>Restricted view</ng-container>
}

5
apps/client/src/app/components/access-table/access-table.component.ts

@ -1,6 +1,7 @@
import { ConfirmationDialogType } from '@ghostfolio/common/enums';
import { Access, User } from '@ghostfolio/common/interfaces';
import { publicRoutes } from '@ghostfolio/common/routes/routes';
import { hasScope, scopes } from '@ghostfolio/common/scopes';
import { NotificationService } from '@ghostfolio/ui/notifications';
import { Clipboard, ClipboardModule } from '@angular/cdk/clipboard';
@ -100,6 +101,10 @@ export class GfAccessTableComponent {
return `${this.baseUrl}/${languageCode}/${publicRoutes.public.path}/${aId}`;
}
protected hasScopeToReadValues({ scopes: scopesOfAccess }: Access) {
return hasScope(scopesOfAccess, scopes.portfolioReadValues);
}
protected onCopyUrlToClipboard(aId: string) {
this.clipboard.copy(this.getPublicUrl(aId));

13
apps/client/src/app/components/account-detail-dialog/account-detail-dialog.component.ts

@ -14,11 +14,8 @@ import {
PortfolioPosition,
User
} from '@ghostfolio/common/interfaces';
import {
hasPermission,
hasReadRestrictedAccessPermission,
permissions
} from '@ghostfolio/common/permissions';
import { hasPermission, permissions } from '@ghostfolio/common/permissions';
import { hasScope, scopes } from '@ghostfolio/common/scopes';
import { GfAccountBalancesComponent } from '@ghostfolio/ui/account-balances';
import { GfActivitiesTableComponent } from '@ghostfolio/ui/activities-table';
import { GfDialogFooterComponent } from '@ghostfolio/ui/dialog-footer';
@ -233,10 +230,8 @@ export class GfAccountDetailDialogComponent implements OnInit {
protected showValuesInPercentage() {
return (
hasReadRestrictedAccessPermission({
accesses: this.user?.access,
impersonationId: this.data.impersonationId
}) || this.user?.settings?.isRestrictedView
!hasScope(this.user?.scopes, scopes.portfolioReadValues) ||
this.user?.settings?.isRestrictedView
);
}

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

@ -511,9 +511,7 @@
</div>
</div>
</mat-tab>
<!-- Splits are not applied to the portfolio calculation yet, hence the
tab to manage them is disabled -->
@if (false && user?.settings?.isExperimentalFeatures) {
@if (user?.settings?.isExperimentalFeatures) {
<mat-tab>
<ng-template mat-tab-label>
<ion-icon name="git-compare-outline" />

3
apps/client/src/app/components/user-account-access/create-or-update-access-dialog/interfaces/interfaces.ts

@ -1,5 +1,6 @@
import { Access } from '@ghostfolio/common/interfaces';
export interface CreateOrUpdateAccessDialogParams {
access?: Access;
// TODO: Include the scopes once the dialog allows to configure them
access?: Omit<Access, 'scopes'>;
}

21
apps/client/src/app/components/user-account-access/user-account-access.component.ts

@ -247,15 +247,18 @@ export class GfUserAccountAccessComponent implements OnInit {
}
private update() {
this.accessesGet = this.user.access.map(({ alias, id, permissions }) => {
return {
id,
permissions,
alias: alias ?? '',
grantee: $localize`Me`,
type: 'PRIVATE'
};
});
this.accessesGet = this.user.access.map(
({ alias, id, permissions, scopes }) => {
return {
id,
permissions,
scopes,
alias: alias ?? '',
grantee: $localize`Me`,
type: 'PRIVATE'
};
}
);
this.dataService
.fetchAccesses()

13
apps/client/src/app/pages/portfolio/allocations/allocations-page.component.ts

@ -17,11 +17,8 @@ import {
PortfolioPosition,
User
} from '@ghostfolio/common/interfaces';
import {
hasPermission,
hasReadRestrictedAccessPermission,
permissions
} from '@ghostfolio/common/permissions';
import { hasPermission, permissions } from '@ghostfolio/common/permissions';
import { hasScope, scopes } from '@ghostfolio/common/scopes';
import { MarketAdvanced } from '@ghostfolio/common/types';
import { translate } from '@ghostfolio/ui/i18n';
import { GfPortfolioProportionChartComponent } from '@ghostfolio/ui/portfolio-proportion-chart';
@ -229,10 +226,8 @@ export class GfAllocationsPageComponent implements OnInit {
protected showValuesInPercentage() {
return (
hasReadRestrictedAccessPermission({
accesses: this.user?.access,
impersonationId: this.impersonationId
}) || this.user?.settings?.isRestrictedView
!hasScope(this.user?.scopes, scopes.portfolioReadValues) ||
this.user?.settings?.isRestrictedView
);
}

27
apps/client/src/app/pages/portfolio/analysis/analysis-page.component.ts

@ -1,6 +1,5 @@
import { GfBenchmarkComparatorComponent } from '@ghostfolio/client/components/benchmark-comparator/benchmark-comparator.component';
import { GfInvestmentChartComponent } from '@ghostfolio/client/components/investment-chart/investment-chart.component';
import { ImpersonationStorageService } from '@ghostfolio/client/services/impersonation-storage.service';
import { UserService } from '@ghostfolio/client/services/user/user.service';
import {
DEFAULT_DATE_RANGE,
@ -15,11 +14,8 @@ import {
PortfolioPosition,
User
} from '@ghostfolio/common/interfaces';
import {
hasPermission,
hasReadRestrictedAccessPermission,
permissions
} from '@ghostfolio/common/permissions';
import { hasPermission, permissions } from '@ghostfolio/common/permissions';
import { hasScope, scopes } from '@ghostfolio/common/scopes';
import type {
AiPromptMode,
GroupBy,
@ -88,7 +84,6 @@ export class GfAnalysisPageComponent implements OnInit {
protected dividendsByGroup: InvestmentItem[];
protected readonly dividendTimelineDataLabel = $localize`Dividend`;
protected hasPermissionToReadAiPrompt: boolean;
protected impersonationId: string | null;
protected investments: InvestmentItem[];
protected readonly investmentTimelineDataLabel = $localize`Invested Capital`;
protected investmentsByGroup: InvestmentItem[];
@ -126,9 +121,6 @@ export class GfAnalysisPageComponent implements OnInit {
private readonly dataService = inject(DataService);
private readonly destroyRef = inject(DestroyRef);
private readonly deviceDetectorService = inject(DeviceDetectorService);
private readonly impersonationStorageService = inject(
ImpersonationStorageService
);
private readonly snackBar = inject(MatSnackBar);
private readonly userService = inject(UserService);
@ -150,15 +142,6 @@ export class GfAnalysisPageComponent implements OnInit {
}
public ngOnInit() {
this.impersonationStorageService
.onChangeHasImpersonation()
.pipe(takeUntilDestroyed(this.destroyRef))
.subscribe((impersonationId) => {
this.impersonationId = impersonationId;
this.changeDetectorRef.markForCheck();
});
this.userService.stateChanged
.pipe(takeUntilDestroyed(this.destroyRef))
.subscribe((state) => {
@ -247,10 +230,8 @@ export class GfAnalysisPageComponent implements OnInit {
protected showValuesInPercentage() {
return (
hasReadRestrictedAccessPermission({
accesses: this.user?.access,
impersonationId: this.impersonationId
}) || this.user?.settings?.isRestrictedView
!hasScope(this.user?.scopes, scopes.portfolioReadValues) ||
this.user?.settings?.isRestrictedView
);
}

60
apps/client/src/locales/messages.ca.xlf

@ -375,7 +375,7 @@
<target state="translated">Realment vol revocar aquest accés?</target>
<context-group purpose="location">
<context context-type="sourcefile">apps/client/src/app/components/access-table/access-table.component.ts</context>
<context context-type="linenumber">121</context>
<context context-type="linenumber">126</context>
</context-group>
</trans-unit>
<trans-unit id="4029903570030679337" datatype="html">
@ -839,7 +839,7 @@
<target state="new">Splits</target>
<context-group purpose="location">
<context context-type="sourcefile">apps/client/src/app/components/admin-market-data/asset-profile-dialog/asset-profile-dialog.html</context>
<context context-type="linenumber">520</context>
<context context-type="linenumber">518</context>
</context-group>
</trans-unit>
<trans-unit id="267346373699222750" datatype="html">
@ -887,11 +887,11 @@
<target state="translated">Data</target>
<context-group purpose="location">
<context context-type="sourcefile">apps/client/src/app/components/admin-market-data/asset-profile-dialog/asset-profile-dialog.html</context>
<context context-type="linenumber">529</context>
<context context-type="linenumber">527</context>
</context-group>
<context-group purpose="location">
<context context-type="sourcefile">apps/client/src/app/components/admin-market-data/asset-profile-dialog/asset-profile-dialog.html</context>
<context context-type="linenumber">568</context>
<context context-type="linenumber">566</context>
</context-group>
<context-group purpose="location">
<context context-type="sourcefile">apps/client/src/app/pages/portfolio/activities/create-or-update-activity-dialog/create-or-update-activity-dialog.html</context>
@ -1103,7 +1103,7 @@
<target state="translated">Oooh! No s’han pogut recopilar les dades históriques.</target>
<context-group purpose="location">
<context context-type="sourcefile">libs/ui/src/lib/historical-market-data-editor/historical-market-data-editor.component.ts</context>
<context context-type="linenumber">284</context>
<context context-type="linenumber">286</context>
</context-group>
</trans-unit>
<trans-unit id="7194017842579795231" datatype="html">
@ -1267,7 +1267,7 @@
<target state="translated">Configuració del Proveïdor de Dades</target>
<context-group purpose="location">
<context context-type="sourcefile">apps/client/src/app/components/admin-market-data/asset-profile-dialog/asset-profile-dialog.html</context>
<context context-type="linenumber">636</context>
<context context-type="linenumber">634</context>
</context-group>
</trans-unit>
<trans-unit id="6563391987554512024" datatype="html">
@ -1275,7 +1275,7 @@
<target state="translated">Prova</target>
<context-group purpose="location">
<context context-type="sourcefile">apps/client/src/app/components/admin-market-data/asset-profile-dialog/asset-profile-dialog.html</context>
<context context-type="linenumber">733</context>
<context context-type="linenumber">731</context>
</context-group>
</trans-unit>
<trans-unit id="8308045076391224954" datatype="html">
@ -1287,7 +1287,7 @@
</context-group>
<context-group purpose="location">
<context context-type="sourcefile">apps/client/src/app/components/admin-market-data/asset-profile-dialog/asset-profile-dialog.html</context>
<context context-type="linenumber">715</context>
<context context-type="linenumber">713</context>
</context-group>
<context-group purpose="location">
<context context-type="sourcefile">apps/client/src/app/components/admin-platform/admin-platform.component.html</context>
@ -1483,7 +1483,7 @@
<target state="translated">Recollida de Dades</target>
<context-group purpose="location">
<context context-type="sourcefile">apps/client/src/app/components/admin-market-data/asset-profile-dialog/asset-profile-dialog.html</context>
<context context-type="linenumber">756</context>
<context context-type="linenumber">754</context>
</context-group>
<context-group purpose="location">
<context context-type="sourcefile">apps/client/src/app/components/admin-overview/admin-overview.html</context>
@ -1527,7 +1527,7 @@
<target state="translated">Afegir</target>
<context-group purpose="location">
<context context-type="sourcefile">apps/client/src/app/components/admin-market-data/asset-profile-dialog/asset-profile-dialog.html</context>
<context context-type="linenumber">623</context>
<context context-type="linenumber">621</context>
</context-group>
<context-group purpose="location">
<context context-type="sourcefile">apps/client/src/app/components/admin-overview/admin-overview.html</context>
@ -1995,7 +1995,7 @@
<target state="new">Split Ratio</target>
<context-group purpose="location">
<context context-type="sourcefile">apps/client/src/app/components/admin-market-data/asset-profile-dialog/asset-profile-dialog.html</context>
<context context-type="linenumber">534</context>
<context context-type="linenumber">532</context>
</context-group>
</trans-unit>
<trans-unit id="5486880308148746399" datatype="html">
@ -2851,7 +2851,7 @@
<target state="translated">Localització</target>
<context-group purpose="location">
<context context-type="sourcefile">apps/client/src/app/components/admin-market-data/asset-profile-dialog/asset-profile-dialog.html</context>
<context context-type="linenumber">674</context>
<context context-type="linenumber">672</context>
</context-group>
<context-group purpose="location">
<context context-type="sourcefile">apps/client/src/app/components/user-account-settings/user-account-settings.html</context>
@ -4620,7 +4620,7 @@
<target state="new">Shares After</target>
<context-group purpose="location">
<context context-type="sourcefile">apps/client/src/app/components/admin-market-data/asset-profile-dialog/asset-profile-dialog.html</context>
<context context-type="linenumber">592</context>
<context context-type="linenumber">590</context>
</context-group>
</trans-unit>
<trans-unit id="8890553633144307762" datatype="html">
@ -4888,7 +4888,7 @@
</context-group>
<context-group purpose="location">
<context context-type="sourcefile">apps/client/src/app/pages/portfolio/analysis/analysis-page.component.ts</context>
<context context-type="linenumber">89</context>
<context context-type="linenumber">85</context>
</context-group>
<context-group purpose="location">
<context context-type="sourcefile">libs/ui/src/lib/i18n.ts</context>
@ -4900,7 +4900,7 @@
<target state="translated">Inversió</target>
<context-group purpose="location">
<context context-type="sourcefile">apps/client/src/app/pages/portfolio/analysis/analysis-page.component.ts</context>
<context context-type="linenumber">109</context>
<context context-type="linenumber">104</context>
</context-group>
<context-group purpose="location">
<context context-type="sourcefile">apps/client/src/app/pages/resources/personal-finance-tools/product-page.component.ts</context>
@ -4920,7 +4920,7 @@
<target state="translated">Mensualment</target>
<context-group purpose="location">
<context context-type="sourcefile">apps/client/src/app/pages/portfolio/analysis/analysis-page.component.ts</context>
<context context-type="linenumber">103</context>
<context context-type="linenumber">98</context>
</context-group>
</trans-unit>
<trans-unit id="8036977202721714375" datatype="html">
@ -4928,7 +4928,7 @@
<target state="translated">Anualment</target>
<context-group purpose="location">
<context context-type="sourcefile">apps/client/src/app/pages/portfolio/analysis/analysis-page.component.ts</context>
<context context-type="linenumber">104</context>
<context context-type="linenumber">99</context>
</context-group>
</trans-unit>
<trans-unit id="1600023202562292052" datatype="html">
@ -6737,7 +6737,7 @@
</context-group>
<context-group purpose="location">
<context context-type="sourcefile">apps/client/src/app/pages/portfolio/analysis/analysis-page.component.ts</context>
<context context-type="linenumber">93</context>
<context context-type="linenumber">88</context>
</context-group>
</trans-unit>
<trans-unit id="5957846001261659229" datatype="html">
@ -7081,7 +7081,7 @@
</context-group>
<context-group purpose="location">
<context context-type="sourcefile">apps/client/src/app/components/admin-market-data/asset-profile-dialog/asset-profile-dialog.html</context>
<context context-type="linenumber">761</context>
<context context-type="linenumber">759</context>
</context-group>
<context-group purpose="location">
<context context-type="sourcefile">apps/client/src/app/components/admin-market-data/create-asset-profile-dialog/create-asset-profile-dialog.html</context>
@ -7133,7 +7133,7 @@
<target state="new">Close</target>
<context-group purpose="location">
<context context-type="sourcefile">apps/client/src/app/components/admin-market-data/asset-profile-dialog/asset-profile-dialog.html</context>
<context context-type="linenumber">763</context>
<context context-type="linenumber">761</context>
</context-group>
<context-group purpose="location">
<context context-type="sourcefile">apps/client/src/app/components/admin-market-data/create-asset-profile-dialog/create-asset-profile-dialog.html</context>
@ -7421,7 +7421,7 @@
<target state="new">Shares Before</target>
<context-group purpose="location">
<context context-type="sourcefile">apps/client/src/app/components/admin-market-data/asset-profile-dialog/asset-profile-dialog.html</context>
<context context-type="linenumber">605</context>
<context context-type="linenumber">603</context>
</context-group>
</trans-unit>
<trans-unit id="4605555143173907624" datatype="html">
@ -7711,7 +7711,7 @@
<target state="new">Save</target>
<context-group purpose="location">
<context context-type="sourcefile">apps/client/src/app/components/admin-market-data/asset-profile-dialog/asset-profile-dialog.html</context>
<context context-type="linenumber">772</context>
<context context-type="linenumber">770</context>
</context-group>
<context-group purpose="location">
<context context-type="sourcefile">apps/client/src/app/components/admin-market-data/create-asset-profile-dialog/create-asset-profile-dialog.html</context>
@ -7763,7 +7763,7 @@
</context-group>
<context-group purpose="location">
<context context-type="sourcefile">apps/client/src/app/components/user-account-access/user-account-access.component.ts</context>
<context context-type="linenumber">255</context>
<context context-type="linenumber">257</context>
</context-group>
</trans-unit>
<trans-unit id="1769610706135259386" datatype="html">
@ -7811,7 +7811,7 @@
<target state="new">AI prompt has been copied to the clipboard</target>
<context-group purpose="location">
<context context-type="sourcefile">apps/client/src/app/pages/portfolio/analysis/analysis-page.component.ts</context>
<context context-type="linenumber">222</context>
<context context-type="linenumber">205</context>
</context-group>
</trans-unit>
<trans-unit id="1616747898909934803" datatype="html">
@ -7819,7 +7819,7 @@
<target state="new">Link has been copied to the clipboard</target>
<context-group purpose="location">
<context context-type="sourcefile">apps/client/src/app/components/access-table/access-table.component.ts</context>
<context context-type="linenumber">107</context>
<context context-type="linenumber">112</context>
</context-group>
</trans-unit>
<trans-unit id="8540986733881734625" datatype="html">
@ -7843,7 +7843,7 @@
<target state="new">Default Market Price</target>
<context-group purpose="location">
<context context-type="sourcefile">apps/client/src/app/components/admin-market-data/asset-profile-dialog/asset-profile-dialog.html</context>
<context context-type="linenumber">646</context>
<context context-type="linenumber">644</context>
</context-group>
</trans-unit>
<trans-unit id="1713271461473302108" datatype="html">
@ -7851,7 +7851,7 @@
<target state="new">Mode</target>
<context-group purpose="location">
<context context-type="sourcefile">apps/client/src/app/components/admin-market-data/asset-profile-dialog/asset-profile-dialog.html</context>
<context context-type="linenumber">683</context>
<context context-type="linenumber">681</context>
</context-group>
</trans-unit>
<trans-unit id="1717822775546188057" datatype="html">
@ -7867,7 +7867,7 @@
<target state="new">Selector</target>
<context-group purpose="location">
<context context-type="sourcefile">apps/client/src/app/components/admin-market-data/asset-profile-dialog/asset-profile-dialog.html</context>
<context context-type="linenumber">699</context>
<context context-type="linenumber">697</context>
</context-group>
</trans-unit>
<trans-unit id="645724892732039501" datatype="html">
@ -7875,7 +7875,7 @@
<target state="new">HTTP Request Headers</target>
<context-group purpose="location">
<context context-type="sourcefile">apps/client/src/app/components/admin-market-data/asset-profile-dialog/asset-profile-dialog.html</context>
<context context-type="linenumber">659</context>
<context context-type="linenumber">657</context>
</context-group>
</trans-unit>
<trans-unit id="8635324470284879211" datatype="html">
@ -7899,7 +7899,7 @@
<target state="new">Open Duck.ai</target>
<context-group purpose="location">
<context context-type="sourcefile">apps/client/src/app/pages/portfolio/analysis/analysis-page.component.ts</context>
<context context-type="linenumber">223</context>
<context context-type="linenumber">206</context>
</context-group>
</trans-unit>
<trans-unit id="5674286808255988565" datatype="html">

60
apps/client/src/locales/messages.de.xlf

@ -102,7 +102,7 @@
<target state="translated">Möchtest du diese Zugangsberechtigung wirklich widerrufen?</target>
<context-group purpose="location">
<context context-type="sourcefile">apps/client/src/app/components/access-table/access-table.component.ts</context>
<context context-type="linenumber">121</context>
<context context-type="linenumber">126</context>
</context-group>
</trans-unit>
<trans-unit id="8953033926734869941" datatype="html">
@ -442,7 +442,7 @@
<target state="translated">Splits</target>
<context-group purpose="location">
<context context-type="sourcefile">apps/client/src/app/components/admin-market-data/asset-profile-dialog/asset-profile-dialog.html</context>
<context context-type="linenumber">520</context>
<context context-type="linenumber">518</context>
</context-group>
</trans-unit>
<trans-unit id="267346373699222750" datatype="html">
@ -482,11 +482,11 @@
<target state="translated">Datum</target>
<context-group purpose="location">
<context context-type="sourcefile">apps/client/src/app/components/admin-market-data/asset-profile-dialog/asset-profile-dialog.html</context>
<context context-type="linenumber">529</context>
<context context-type="linenumber">527</context>
</context-group>
<context-group purpose="location">
<context context-type="sourcefile">apps/client/src/app/components/admin-market-data/asset-profile-dialog/asset-profile-dialog.html</context>
<context context-type="linenumber">568</context>
<context context-type="linenumber">566</context>
</context-group>
<context-group purpose="location">
<context context-type="sourcefile">apps/client/src/app/pages/portfolio/activities/create-or-update-activity-dialog/create-or-update-activity-dialog.html</context>
@ -674,7 +674,7 @@
<target state="translated">Hinzufügen</target>
<context-group purpose="location">
<context context-type="sourcefile">apps/client/src/app/components/admin-market-data/asset-profile-dialog/asset-profile-dialog.html</context>
<context context-type="linenumber">623</context>
<context context-type="linenumber">621</context>
</context-group>
<context-group purpose="location">
<context context-type="sourcefile">apps/client/src/app/components/admin-overview/admin-overview.html</context>
@ -866,7 +866,7 @@
<target state="translated">Split-Verhältnis</target>
<context-group purpose="location">
<context context-type="sourcefile">apps/client/src/app/components/admin-market-data/asset-profile-dialog/asset-profile-dialog.html</context>
<context context-type="linenumber">534</context>
<context context-type="linenumber">532</context>
</context-group>
</trans-unit>
<trans-unit id="103717583691217276" datatype="html">
@ -1398,7 +1398,7 @@
<target state="translated">Lokalität</target>
<context-group purpose="location">
<context context-type="sourcefile">apps/client/src/app/components/admin-market-data/asset-profile-dialog/asset-profile-dialog.html</context>
<context context-type="linenumber">674</context>
<context context-type="linenumber">672</context>
</context-group>
<context-group purpose="location">
<context context-type="sourcefile">apps/client/src/app/components/user-account-settings/user-account-settings.html</context>
@ -2746,7 +2746,7 @@
<target state="translated">Monatlich</target>
<context-group purpose="location">
<context context-type="sourcefile">apps/client/src/app/pages/portfolio/analysis/analysis-page.component.ts</context>
<context context-type="linenumber">103</context>
<context context-type="linenumber">98</context>
</context-group>
</trans-unit>
<trans-unit id="5213771062241898526" datatype="html">
@ -3370,7 +3370,7 @@
</context-group>
<context-group purpose="location">
<context context-type="sourcefile">apps/client/src/app/pages/portfolio/analysis/analysis-page.component.ts</context>
<context context-type="linenumber">93</context>
<context context-type="linenumber">88</context>
</context-group>
</trans-unit>
<trans-unit id="5957846001261659229" datatype="html">
@ -3490,7 +3490,7 @@
</context-group>
<context-group purpose="location">
<context context-type="sourcefile">apps/client/src/app/pages/portfolio/analysis/analysis-page.component.ts</context>
<context context-type="linenumber">89</context>
<context context-type="linenumber">85</context>
</context-group>
<context-group purpose="location">
<context context-type="sourcefile">libs/ui/src/lib/i18n.ts</context>
@ -3626,7 +3626,7 @@
<target state="translated">Jährlich</target>
<context-group purpose="location">
<context context-type="sourcefile">apps/client/src/app/pages/portfolio/analysis/analysis-page.component.ts</context>
<context context-type="linenumber">104</context>
<context context-type="linenumber">99</context>
</context-group>
</trans-unit>
<trans-unit id="72640258012696878" datatype="html">
@ -4234,7 +4234,7 @@
</context-group>
<context-group purpose="location">
<context context-type="sourcefile">apps/client/src/app/components/admin-market-data/asset-profile-dialog/asset-profile-dialog.html</context>
<context context-type="linenumber">715</context>
<context context-type="linenumber">713</context>
</context-group>
<context-group purpose="location">
<context context-type="sourcefile">apps/client/src/app/components/admin-platform/admin-platform.component.html</context>
@ -4386,7 +4386,7 @@
<target state="translated">Anteile nachher</target>
<context-group purpose="location">
<context context-type="sourcefile">apps/client/src/app/components/admin-market-data/asset-profile-dialog/asset-profile-dialog.html</context>
<context context-type="linenumber">592</context>
<context context-type="linenumber">590</context>
</context-group>
</trans-unit>
<trans-unit id="5393065248443550644" datatype="html">
@ -4658,7 +4658,7 @@
<target state="translated">Scraper Konfiguration</target>
<context-group purpose="location">
<context context-type="sourcefile">apps/client/src/app/components/admin-market-data/asset-profile-dialog/asset-profile-dialog.html</context>
<context context-type="linenumber">636</context>
<context context-type="linenumber">634</context>
</context-group>
</trans-unit>
<trans-unit id="7504169991280318133" datatype="html">
@ -6180,7 +6180,7 @@
<target state="translated">Ups! Die historischen Daten konnten nicht geparsed werden.</target>
<context-group purpose="location">
<context context-type="sourcefile">libs/ui/src/lib/historical-market-data-editor/historical-market-data-editor.component.ts</context>
<context context-type="linenumber">284</context>
<context context-type="linenumber">286</context>
</context-group>
</trans-unit>
<trans-unit id="7194017842579795231" datatype="html">
@ -6272,7 +6272,7 @@
<target state="translated">Test</target>
<context-group purpose="location">
<context context-type="sourcefile">apps/client/src/app/components/admin-market-data/asset-profile-dialog/asset-profile-dialog.html</context>
<context context-type="linenumber">733</context>
<context context-type="linenumber">731</context>
</context-group>
</trans-unit>
<trans-unit id="6483680626836794824" datatype="html">
@ -6348,7 +6348,7 @@
<target state="translated">Investition</target>
<context-group purpose="location">
<context context-type="sourcefile">apps/client/src/app/pages/portfolio/analysis/analysis-page.component.ts</context>
<context context-type="linenumber">109</context>
<context context-type="linenumber">104</context>
</context-group>
<context-group purpose="location">
<context context-type="sourcefile">apps/client/src/app/pages/resources/personal-finance-tools/product-page.component.ts</context>
@ -6540,7 +6540,7 @@
<target state="translated">Finanzmarktdaten synchronisieren</target>
<context-group purpose="location">
<context context-type="sourcefile">apps/client/src/app/components/admin-market-data/asset-profile-dialog/asset-profile-dialog.html</context>
<context context-type="linenumber">756</context>
<context context-type="linenumber">754</context>
</context-group>
<context-group purpose="location">
<context context-type="sourcefile">apps/client/src/app/components/admin-overview/admin-overview.html</context>
@ -7105,7 +7105,7 @@
</context-group>
<context-group purpose="location">
<context context-type="sourcefile">apps/client/src/app/components/admin-market-data/asset-profile-dialog/asset-profile-dialog.html</context>
<context context-type="linenumber">761</context>
<context context-type="linenumber">759</context>
</context-group>
<context-group purpose="location">
<context context-type="sourcefile">apps/client/src/app/components/admin-market-data/create-asset-profile-dialog/create-asset-profile-dialog.html</context>
@ -7157,7 +7157,7 @@
<target state="translated">Schliessen</target>
<context-group purpose="location">
<context context-type="sourcefile">apps/client/src/app/components/admin-market-data/asset-profile-dialog/asset-profile-dialog.html</context>
<context context-type="linenumber">763</context>
<context context-type="linenumber">761</context>
</context-group>
<context-group purpose="location">
<context context-type="sourcefile">apps/client/src/app/components/admin-market-data/create-asset-profile-dialog/create-asset-profile-dialog.html</context>
@ -7445,7 +7445,7 @@
<target state="translated">Anteile vorher</target>
<context-group purpose="location">
<context context-type="sourcefile">apps/client/src/app/components/admin-market-data/asset-profile-dialog/asset-profile-dialog.html</context>
<context context-type="linenumber">605</context>
<context context-type="linenumber">603</context>
</context-group>
</trans-unit>
<trans-unit id="4605555143173907624" datatype="html">
@ -7735,7 +7735,7 @@
<target state="translated">Speichern</target>
<context-group purpose="location">
<context context-type="sourcefile">apps/client/src/app/components/admin-market-data/asset-profile-dialog/asset-profile-dialog.html</context>
<context context-type="linenumber">772</context>
<context context-type="linenumber">770</context>
</context-group>
<context-group purpose="location">
<context context-type="sourcefile">apps/client/src/app/components/admin-market-data/create-asset-profile-dialog/create-asset-profile-dialog.html</context>
@ -7787,7 +7787,7 @@
</context-group>
<context-group purpose="location">
<context context-type="sourcefile">apps/client/src/app/components/user-account-access/user-account-access.component.ts</context>
<context context-type="linenumber">255</context>
<context context-type="linenumber">257</context>
</context-group>
</trans-unit>
<trans-unit id="1769610706135259386" datatype="html">
@ -7835,7 +7835,7 @@
<target state="translated">KI-Anweisung wurde in die Zwischenablage kopiert</target>
<context-group purpose="location">
<context context-type="sourcefile">apps/client/src/app/pages/portfolio/analysis/analysis-page.component.ts</context>
<context context-type="linenumber">222</context>
<context context-type="linenumber">205</context>
</context-group>
</trans-unit>
<trans-unit id="1616747898909934803" datatype="html">
@ -7843,7 +7843,7 @@
<target state="translated">Link wurde in die Zwischenablage kopiert</target>
<context-group purpose="location">
<context context-type="sourcefile">apps/client/src/app/components/access-table/access-table.component.ts</context>
<context context-type="linenumber">107</context>
<context context-type="linenumber">112</context>
</context-group>
</trans-unit>
<trans-unit id="8540986733881734625" datatype="html">
@ -7867,7 +7867,7 @@
<target state="translated">Standardmarktpreis</target>
<context-group purpose="location">
<context context-type="sourcefile">apps/client/src/app/components/admin-market-data/asset-profile-dialog/asset-profile-dialog.html</context>
<context context-type="linenumber">646</context>
<context context-type="linenumber">644</context>
</context-group>
</trans-unit>
<trans-unit id="1713271461473302108" datatype="html">
@ -7875,7 +7875,7 @@
<target state="translated">Modus</target>
<context-group purpose="location">
<context context-type="sourcefile">apps/client/src/app/components/admin-market-data/asset-profile-dialog/asset-profile-dialog.html</context>
<context context-type="linenumber">683</context>
<context context-type="linenumber">681</context>
</context-group>
</trans-unit>
<trans-unit id="1717822775546188057" datatype="html">
@ -7891,7 +7891,7 @@
<target state="translated">Selektor</target>
<context-group purpose="location">
<context context-type="sourcefile">apps/client/src/app/components/admin-market-data/asset-profile-dialog/asset-profile-dialog.html</context>
<context context-type="linenumber">699</context>
<context context-type="linenumber">697</context>
</context-group>
</trans-unit>
<trans-unit id="645724892732039501" datatype="html">
@ -7899,7 +7899,7 @@
<target state="translated">HTTP Request-Headers</target>
<context-group purpose="location">
<context context-type="sourcefile">apps/client/src/app/components/admin-market-data/asset-profile-dialog/asset-profile-dialog.html</context>
<context context-type="linenumber">659</context>
<context context-type="linenumber">657</context>
</context-group>
</trans-unit>
<trans-unit id="8635324470284879211" datatype="html">
@ -7923,7 +7923,7 @@
<target state="translated">Öffne Duck.ai</target>
<context-group purpose="location">
<context context-type="sourcefile">apps/client/src/app/pages/portfolio/analysis/analysis-page.component.ts</context>
<context context-type="linenumber">223</context>
<context context-type="linenumber">206</context>
</context-group>
</trans-unit>
<trans-unit id="5674286808255988565" datatype="html">

60
apps/client/src/locales/messages.es.xlf

@ -103,7 +103,7 @@
<target state="translated">¿Seguro que quieres revocar el acceso concedido?</target>
<context-group purpose="location">
<context context-type="sourcefile">apps/client/src/app/components/access-table/access-table.component.ts</context>
<context context-type="linenumber">121</context>
<context context-type="linenumber">126</context>
</context-group>
</trans-unit>
<trans-unit id="8953033926734869941" datatype="html">
@ -443,7 +443,7 @@
<target state="translated">Desdoblamientos</target>
<context-group purpose="location">
<context context-type="sourcefile">apps/client/src/app/components/admin-market-data/asset-profile-dialog/asset-profile-dialog.html</context>
<context context-type="linenumber">520</context>
<context context-type="linenumber">518</context>
</context-group>
</trans-unit>
<trans-unit id="267346373699222750" datatype="html">
@ -483,11 +483,11 @@
<target state="translated">Fecha</target>
<context-group purpose="location">
<context context-type="sourcefile">apps/client/src/app/components/admin-market-data/asset-profile-dialog/asset-profile-dialog.html</context>
<context context-type="linenumber">529</context>
<context context-type="linenumber">527</context>
</context-group>
<context-group purpose="location">
<context context-type="sourcefile">apps/client/src/app/components/admin-market-data/asset-profile-dialog/asset-profile-dialog.html</context>
<context context-type="linenumber">568</context>
<context context-type="linenumber">566</context>
</context-group>
<context-group purpose="location">
<context context-type="sourcefile">apps/client/src/app/pages/portfolio/activities/create-or-update-activity-dialog/create-or-update-activity-dialog.html</context>
@ -659,7 +659,7 @@
<target state="translated">Añadir</target>
<context-group purpose="location">
<context context-type="sourcefile">apps/client/src/app/components/admin-market-data/asset-profile-dialog/asset-profile-dialog.html</context>
<context context-type="linenumber">623</context>
<context context-type="linenumber">621</context>
</context-group>
<context-group purpose="location">
<context context-type="sourcefile">apps/client/src/app/components/admin-overview/admin-overview.html</context>
@ -851,7 +851,7 @@
<target state="translated">Relación de desdoblamiento</target>
<context-group purpose="location">
<context context-type="sourcefile">apps/client/src/app/components/admin-market-data/asset-profile-dialog/asset-profile-dialog.html</context>
<context context-type="linenumber">534</context>
<context context-type="linenumber">532</context>
</context-group>
</trans-unit>
<trans-unit id="103717583691217276" datatype="html">
@ -1383,7 +1383,7 @@
<target state="translated">Configuración regional</target>
<context-group purpose="location">
<context context-type="sourcefile">apps/client/src/app/components/admin-market-data/asset-profile-dialog/asset-profile-dialog.html</context>
<context context-type="linenumber">674</context>
<context context-type="linenumber">672</context>
</context-group>
<context-group purpose="location">
<context context-type="sourcefile">apps/client/src/app/components/user-account-settings/user-account-settings.html</context>
@ -2767,7 +2767,7 @@
<target state="translated">Mensual</target>
<context-group purpose="location">
<context context-type="sourcefile">apps/client/src/app/pages/portfolio/analysis/analysis-page.component.ts</context>
<context context-type="linenumber">103</context>
<context context-type="linenumber">98</context>
</context-group>
</trans-unit>
<trans-unit id="6130372166370766747" datatype="html">
@ -3355,7 +3355,7 @@
</context-group>
<context-group purpose="location">
<context context-type="sourcefile">apps/client/src/app/pages/portfolio/analysis/analysis-page.component.ts</context>
<context context-type="linenumber">93</context>
<context context-type="linenumber">88</context>
</context-group>
</trans-unit>
<trans-unit id="5957846001261659229" datatype="html">
@ -3467,7 +3467,7 @@
</context-group>
<context-group purpose="location">
<context context-type="sourcefile">apps/client/src/app/pages/portfolio/analysis/analysis-page.component.ts</context>
<context context-type="linenumber">89</context>
<context context-type="linenumber">85</context>
</context-group>
<context-group purpose="location">
<context context-type="sourcefile">libs/ui/src/lib/i18n.ts</context>
@ -3611,7 +3611,7 @@
<target state="translated">Anual</target>
<context-group purpose="location">
<context context-type="sourcefile">apps/client/src/app/pages/portfolio/analysis/analysis-page.component.ts</context>
<context context-type="linenumber">104</context>
<context context-type="linenumber">99</context>
</context-group>
</trans-unit>
<trans-unit id="72640258012696878" datatype="html">
@ -4211,7 +4211,7 @@
</context-group>
<context-group purpose="location">
<context context-type="sourcefile">apps/client/src/app/components/admin-market-data/asset-profile-dialog/asset-profile-dialog.html</context>
<context context-type="linenumber">715</context>
<context context-type="linenumber">713</context>
</context-group>
<context-group purpose="location">
<context context-type="sourcefile">apps/client/src/app/components/admin-platform/admin-platform.component.html</context>
@ -4363,7 +4363,7 @@
<target state="translated">Acciones después</target>
<context-group purpose="location">
<context context-type="sourcefile">apps/client/src/app/components/admin-market-data/asset-profile-dialog/asset-profile-dialog.html</context>
<context context-type="linenumber">592</context>
<context context-type="linenumber">590</context>
</context-group>
</trans-unit>
<trans-unit id="5393065248443550644" datatype="html">
@ -4635,7 +4635,7 @@
<target state="translated">Configuración del scraper</target>
<context-group purpose="location">
<context context-type="sourcefile">apps/client/src/app/components/admin-market-data/asset-profile-dialog/asset-profile-dialog.html</context>
<context context-type="linenumber">636</context>
<context context-type="linenumber">634</context>
</context-group>
</trans-unit>
<trans-unit id="7504169991280318133" datatype="html">
@ -6157,7 +6157,7 @@
<target state="translated">¡Vaya! No se pudieron analizar los datos históricos.</target>
<context-group purpose="location">
<context context-type="sourcefile">libs/ui/src/lib/historical-market-data-editor/historical-market-data-editor.component.ts</context>
<context context-type="linenumber">284</context>
<context context-type="linenumber">286</context>
</context-group>
</trans-unit>
<trans-unit id="7194017842579795231" datatype="html">
@ -6249,7 +6249,7 @@
<target state="translated">Prueba</target>
<context-group purpose="location">
<context context-type="sourcefile">apps/client/src/app/components/admin-market-data/asset-profile-dialog/asset-profile-dialog.html</context>
<context context-type="linenumber">733</context>
<context context-type="linenumber">731</context>
</context-group>
</trans-unit>
<trans-unit id="6483680626836794824" datatype="html">
@ -6325,7 +6325,7 @@
<target state="translated">Inversión</target>
<context-group purpose="location">
<context context-type="sourcefile">apps/client/src/app/pages/portfolio/analysis/analysis-page.component.ts</context>
<context context-type="linenumber">109</context>
<context context-type="linenumber">104</context>
</context-group>
<context-group purpose="location">
<context context-type="sourcefile">apps/client/src/app/pages/resources/personal-finance-tools/product-page.component.ts</context>
@ -6517,7 +6517,7 @@
<target state="translated">Recopilación de datos</target>
<context-group purpose="location">
<context context-type="sourcefile">apps/client/src/app/components/admin-market-data/asset-profile-dialog/asset-profile-dialog.html</context>
<context context-type="linenumber">756</context>
<context context-type="linenumber">754</context>
</context-group>
<context-group purpose="location">
<context context-type="sourcefile">apps/client/src/app/components/admin-overview/admin-overview.html</context>
@ -7082,7 +7082,7 @@
</context-group>
<context-group purpose="location">
<context context-type="sourcefile">apps/client/src/app/components/admin-market-data/asset-profile-dialog/asset-profile-dialog.html</context>
<context context-type="linenumber">761</context>
<context context-type="linenumber">759</context>
</context-group>
<context-group purpose="location">
<context context-type="sourcefile">apps/client/src/app/components/admin-market-data/create-asset-profile-dialog/create-asset-profile-dialog.html</context>
@ -7134,7 +7134,7 @@
<target state="translated">Cerrar</target>
<context-group purpose="location">
<context context-type="sourcefile">apps/client/src/app/components/admin-market-data/asset-profile-dialog/asset-profile-dialog.html</context>
<context context-type="linenumber">763</context>
<context context-type="linenumber">761</context>
</context-group>
<context-group purpose="location">
<context context-type="sourcefile">apps/client/src/app/components/admin-market-data/create-asset-profile-dialog/create-asset-profile-dialog.html</context>
@ -7422,7 +7422,7 @@
<target state="translated">Acciones antes</target>
<context-group purpose="location">
<context context-type="sourcefile">apps/client/src/app/components/admin-market-data/asset-profile-dialog/asset-profile-dialog.html</context>
<context context-type="linenumber">605</context>
<context context-type="linenumber">603</context>
</context-group>
</trans-unit>
<trans-unit id="4605555143173907624" datatype="html">
@ -7712,7 +7712,7 @@
<target state="translated">Guardar</target>
<context-group purpose="location">
<context context-type="sourcefile">apps/client/src/app/components/admin-market-data/asset-profile-dialog/asset-profile-dialog.html</context>
<context context-type="linenumber">772</context>
<context context-type="linenumber">770</context>
</context-group>
<context-group purpose="location">
<context context-type="sourcefile">apps/client/src/app/components/admin-market-data/create-asset-profile-dialog/create-asset-profile-dialog.html</context>
@ -7764,7 +7764,7 @@
</context-group>
<context-group purpose="location">
<context context-type="sourcefile">apps/client/src/app/components/user-account-access/user-account-access.component.ts</context>
<context context-type="linenumber">255</context>
<context context-type="linenumber">257</context>
</context-group>
</trans-unit>
<trans-unit id="1769610706135259386" datatype="html">
@ -7812,7 +7812,7 @@
<target state="translated">El prompt para la IA ha sido copiado al portapapeles</target>
<context-group purpose="location">
<context context-type="sourcefile">apps/client/src/app/pages/portfolio/analysis/analysis-page.component.ts</context>
<context context-type="linenumber">222</context>
<context context-type="linenumber">205</context>
</context-group>
</trans-unit>
<trans-unit id="1616747898909934803" datatype="html">
@ -7820,7 +7820,7 @@
<target state="translated">El enlace ha sido copiado al portapapeles</target>
<context-group purpose="location">
<context context-type="sourcefile">apps/client/src/app/components/access-table/access-table.component.ts</context>
<context context-type="linenumber">107</context>
<context context-type="linenumber">112</context>
</context-group>
</trans-unit>
<trans-unit id="8540986733881734625" datatype="html">
@ -7844,7 +7844,7 @@
<target state="translated">Precio de mercado por defecto</target>
<context-group purpose="location">
<context context-type="sourcefile">apps/client/src/app/components/admin-market-data/asset-profile-dialog/asset-profile-dialog.html</context>
<context context-type="linenumber">646</context>
<context context-type="linenumber">644</context>
</context-group>
</trans-unit>
<trans-unit id="1713271461473302108" datatype="html">
@ -7852,7 +7852,7 @@
<target state="translated">Modo</target>
<context-group purpose="location">
<context context-type="sourcefile">apps/client/src/app/components/admin-market-data/asset-profile-dialog/asset-profile-dialog.html</context>
<context context-type="linenumber">683</context>
<context context-type="linenumber">681</context>
</context-group>
</trans-unit>
<trans-unit id="1717822775546188057" datatype="html">
@ -7868,7 +7868,7 @@
<target state="translated">Selector</target>
<context-group purpose="location">
<context context-type="sourcefile">apps/client/src/app/components/admin-market-data/asset-profile-dialog/asset-profile-dialog.html</context>
<context context-type="linenumber">699</context>
<context context-type="linenumber">697</context>
</context-group>
</trans-unit>
<trans-unit id="645724892732039501" datatype="html">
@ -7876,7 +7876,7 @@
<target state="translated">Encabezados de solicitud HTTP</target>
<context-group purpose="location">
<context context-type="sourcefile">apps/client/src/app/components/admin-market-data/asset-profile-dialog/asset-profile-dialog.html</context>
<context context-type="linenumber">659</context>
<context context-type="linenumber">657</context>
</context-group>
</trans-unit>
<trans-unit id="8635324470284879211" datatype="html">
@ -7900,7 +7900,7 @@
<target state="translated">Abrir Duck.ai</target>
<context-group purpose="location">
<context context-type="sourcefile">apps/client/src/app/pages/portfolio/analysis/analysis-page.component.ts</context>
<context context-type="linenumber">223</context>
<context context-type="linenumber">206</context>
</context-group>
</trans-unit>
<trans-unit id="5674286808255988565" datatype="html">

60
apps/client/src/locales/messages.fr.xlf

@ -94,7 +94,7 @@
<target state="translated">Voulez-vous vraiment révoquer cet accès ?</target>
<context-group purpose="location">
<context context-type="sourcefile">apps/client/src/app/components/access-table/access-table.component.ts</context>
<context context-type="linenumber">121</context>
<context context-type="linenumber">126</context>
</context-group>
</trans-unit>
<trans-unit id="962765493084218985" datatype="html">
@ -506,7 +506,7 @@
<target state="new">Splits</target>
<context-group purpose="location">
<context context-type="sourcefile">apps/client/src/app/components/admin-market-data/asset-profile-dialog/asset-profile-dialog.html</context>
<context context-type="linenumber">520</context>
<context context-type="linenumber">518</context>
</context-group>
</trans-unit>
<trans-unit id="267346373699222750" datatype="html">
@ -546,11 +546,11 @@
<target state="translated">Date</target>
<context-group purpose="location">
<context context-type="sourcefile">apps/client/src/app/components/admin-market-data/asset-profile-dialog/asset-profile-dialog.html</context>
<context context-type="linenumber">529</context>
<context context-type="linenumber">527</context>
</context-group>
<context-group purpose="location">
<context context-type="sourcefile">apps/client/src/app/components/admin-market-data/asset-profile-dialog/asset-profile-dialog.html</context>
<context context-type="linenumber">568</context>
<context context-type="linenumber">566</context>
</context-group>
<context-group purpose="location">
<context context-type="sourcefile">apps/client/src/app/pages/portfolio/activities/create-or-update-activity-dialog/create-or-update-activity-dialog.html</context>
@ -918,7 +918,7 @@
<target state="translated">Ajouter</target>
<context-group purpose="location">
<context context-type="sourcefile">apps/client/src/app/components/admin-market-data/asset-profile-dialog/asset-profile-dialog.html</context>
<context context-type="linenumber">623</context>
<context context-type="linenumber">621</context>
</context-group>
<context-group purpose="location">
<context context-type="sourcefile">apps/client/src/app/components/admin-overview/admin-overview.html</context>
@ -1122,7 +1122,7 @@
<target state="new">Split Ratio</target>
<context-group purpose="location">
<context context-type="sourcefile">apps/client/src/app/components/admin-market-data/asset-profile-dialog/asset-profile-dialog.html</context>
<context context-type="linenumber">534</context>
<context context-type="linenumber">532</context>
</context-group>
</trans-unit>
<trans-unit id="5486880308148746399" datatype="html">
@ -1718,7 +1718,7 @@
<target state="translated">Paramètres régionaux</target>
<context-group purpose="location">
<context context-type="sourcefile">apps/client/src/app/components/admin-market-data/asset-profile-dialog/asset-profile-dialog.html</context>
<context context-type="linenumber">674</context>
<context context-type="linenumber">672</context>
</context-group>
<context-group purpose="location">
<context context-type="sourcefile">apps/client/src/app/components/user-account-settings/user-account-settings.html</context>
@ -2662,7 +2662,7 @@
</context-group>
<context-group purpose="location">
<context context-type="sourcefile">apps/client/src/app/pages/portfolio/analysis/analysis-page.component.ts</context>
<context context-type="linenumber">89</context>
<context context-type="linenumber">85</context>
</context-group>
<context-group purpose="location">
<context context-type="sourcefile">libs/ui/src/lib/i18n.ts</context>
@ -2682,7 +2682,7 @@
<target state="translated">Mensuel</target>
<context-group purpose="location">
<context context-type="sourcefile">apps/client/src/app/pages/portfolio/analysis/analysis-page.component.ts</context>
<context context-type="linenumber">103</context>
<context context-type="linenumber">98</context>
</context-group>
</trans-unit>
<trans-unit id="6293970137138896363" datatype="html">
@ -3538,7 +3538,7 @@
</context-group>
<context-group purpose="location">
<context context-type="sourcefile">apps/client/src/app/pages/portfolio/analysis/analysis-page.component.ts</context>
<context context-type="linenumber">93</context>
<context context-type="linenumber">88</context>
</context-group>
</trans-unit>
<trans-unit id="5957846001261659229" datatype="html">
@ -3610,7 +3610,7 @@
<target state="translated">Annuel</target>
<context-group purpose="location">
<context context-type="sourcefile">apps/client/src/app/pages/portfolio/analysis/analysis-page.component.ts</context>
<context context-type="linenumber">104</context>
<context context-type="linenumber">99</context>
</context-group>
</trans-unit>
<trans-unit id="72640258012696878" datatype="html">
@ -4210,7 +4210,7 @@
</context-group>
<context-group purpose="location">
<context context-type="sourcefile">apps/client/src/app/components/admin-market-data/asset-profile-dialog/asset-profile-dialog.html</context>
<context context-type="linenumber">715</context>
<context context-type="linenumber">713</context>
</context-group>
<context-group purpose="location">
<context context-type="sourcefile">apps/client/src/app/components/admin-platform/admin-platform.component.html</context>
@ -4362,7 +4362,7 @@
<target state="new">Shares After</target>
<context-group purpose="location">
<context context-type="sourcefile">apps/client/src/app/components/admin-market-data/asset-profile-dialog/asset-profile-dialog.html</context>
<context context-type="linenumber">592</context>
<context context-type="linenumber">590</context>
</context-group>
</trans-unit>
<trans-unit id="5393065248443550644" datatype="html">
@ -4634,7 +4634,7 @@
<target state="translated">Configuration du Scraper</target>
<context-group purpose="location">
<context context-type="sourcefile">apps/client/src/app/components/admin-market-data/asset-profile-dialog/asset-profile-dialog.html</context>
<context context-type="linenumber">636</context>
<context context-type="linenumber">634</context>
</context-group>
</trans-unit>
<trans-unit id="7504169991280318133" datatype="html">
@ -6156,7 +6156,7 @@
<target state="translated">Oops! Echec du parsing des données historiques.</target>
<context-group purpose="location">
<context context-type="sourcefile">libs/ui/src/lib/historical-market-data-editor/historical-market-data-editor.component.ts</context>
<context context-type="linenumber">284</context>
<context context-type="linenumber">286</context>
</context-group>
</trans-unit>
<trans-unit id="7194017842579795231" datatype="html">
@ -6248,7 +6248,7 @@
<target state="translated">Test</target>
<context-group purpose="location">
<context context-type="sourcefile">apps/client/src/app/components/admin-market-data/asset-profile-dialog/asset-profile-dialog.html</context>
<context context-type="linenumber">733</context>
<context context-type="linenumber">731</context>
</context-group>
</trans-unit>
<trans-unit id="6483680626836794824" datatype="html">
@ -6324,7 +6324,7 @@
<target state="translated">Investissement</target>
<context-group purpose="location">
<context context-type="sourcefile">apps/client/src/app/pages/portfolio/analysis/analysis-page.component.ts</context>
<context context-type="linenumber">109</context>
<context context-type="linenumber">104</context>
</context-group>
<context-group purpose="location">
<context context-type="sourcefile">apps/client/src/app/pages/resources/personal-finance-tools/product-page.component.ts</context>
@ -6516,7 +6516,7 @@
<target state="translated">Collecter les données</target>
<context-group purpose="location">
<context context-type="sourcefile">apps/client/src/app/components/admin-market-data/asset-profile-dialog/asset-profile-dialog.html</context>
<context context-type="linenumber">756</context>
<context context-type="linenumber">754</context>
</context-group>
<context-group purpose="location">
<context context-type="sourcefile">apps/client/src/app/components/admin-overview/admin-overview.html</context>
@ -7081,7 +7081,7 @@
</context-group>
<context-group purpose="location">
<context context-type="sourcefile">apps/client/src/app/components/admin-market-data/asset-profile-dialog/asset-profile-dialog.html</context>
<context context-type="linenumber">761</context>
<context context-type="linenumber">759</context>
</context-group>
<context-group purpose="location">
<context context-type="sourcefile">apps/client/src/app/components/admin-market-data/create-asset-profile-dialog/create-asset-profile-dialog.html</context>
@ -7133,7 +7133,7 @@
<target state="translated">Fermer</target>
<context-group purpose="location">
<context context-type="sourcefile">apps/client/src/app/components/admin-market-data/asset-profile-dialog/asset-profile-dialog.html</context>
<context context-type="linenumber">763</context>
<context context-type="linenumber">761</context>
</context-group>
<context-group purpose="location">
<context context-type="sourcefile">apps/client/src/app/components/admin-market-data/create-asset-profile-dialog/create-asset-profile-dialog.html</context>
@ -7421,7 +7421,7 @@
<target state="new">Shares Before</target>
<context-group purpose="location">
<context context-type="sourcefile">apps/client/src/app/components/admin-market-data/asset-profile-dialog/asset-profile-dialog.html</context>
<context context-type="linenumber">605</context>
<context context-type="linenumber">603</context>
</context-group>
</trans-unit>
<trans-unit id="4605555143173907624" datatype="html">
@ -7711,7 +7711,7 @@
<target state="translated">Sauvegarder</target>
<context-group purpose="location">
<context context-type="sourcefile">apps/client/src/app/components/admin-market-data/asset-profile-dialog/asset-profile-dialog.html</context>
<context context-type="linenumber">772</context>
<context context-type="linenumber">770</context>
</context-group>
<context-group purpose="location">
<context context-type="sourcefile">apps/client/src/app/components/admin-market-data/create-asset-profile-dialog/create-asset-profile-dialog.html</context>
@ -7763,7 +7763,7 @@
</context-group>
<context-group purpose="location">
<context context-type="sourcefile">apps/client/src/app/components/user-account-access/user-account-access.component.ts</context>
<context context-type="linenumber">255</context>
<context context-type="linenumber">257</context>
</context-group>
</trans-unit>
<trans-unit id="1769610706135259386" datatype="html">
@ -7811,7 +7811,7 @@
<target state="translated">Le prompt IA a été copié dans le presse-papiers</target>
<context-group purpose="location">
<context context-type="sourcefile">apps/client/src/app/pages/portfolio/analysis/analysis-page.component.ts</context>
<context context-type="linenumber">222</context>
<context context-type="linenumber">205</context>
</context-group>
</trans-unit>
<trans-unit id="1616747898909934803" datatype="html">
@ -7819,7 +7819,7 @@
<target state="translated">Le lien a été copié dans le presse-papiers</target>
<context-group purpose="location">
<context context-type="sourcefile">apps/client/src/app/components/access-table/access-table.component.ts</context>
<context context-type="linenumber">107</context>
<context context-type="linenumber">112</context>
</context-group>
</trans-unit>
<trans-unit id="8540986733881734625" datatype="html">
@ -7843,7 +7843,7 @@
<target state="translated">Prix du marché par défaut</target>
<context-group purpose="location">
<context context-type="sourcefile">apps/client/src/app/components/admin-market-data/asset-profile-dialog/asset-profile-dialog.html</context>
<context context-type="linenumber">646</context>
<context context-type="linenumber">644</context>
</context-group>
</trans-unit>
<trans-unit id="1713271461473302108" datatype="html">
@ -7851,7 +7851,7 @@
<target state="translated">Mode</target>
<context-group purpose="location">
<context context-type="sourcefile">apps/client/src/app/components/admin-market-data/asset-profile-dialog/asset-profile-dialog.html</context>
<context context-type="linenumber">683</context>
<context context-type="linenumber">681</context>
</context-group>
</trans-unit>
<trans-unit id="1717822775546188057" datatype="html">
@ -7867,7 +7867,7 @@
<target state="translated">Selecteur</target>
<context-group purpose="location">
<context context-type="sourcefile">apps/client/src/app/components/admin-market-data/asset-profile-dialog/asset-profile-dialog.html</context>
<context context-type="linenumber">699</context>
<context context-type="linenumber">697</context>
</context-group>
</trans-unit>
<trans-unit id="645724892732039501" datatype="html">
@ -7875,7 +7875,7 @@
<target state="translated">En-têtes de requête HTTP</target>
<context-group purpose="location">
<context context-type="sourcefile">apps/client/src/app/components/admin-market-data/asset-profile-dialog/asset-profile-dialog.html</context>
<context context-type="linenumber">659</context>
<context context-type="linenumber">657</context>
</context-group>
</trans-unit>
<trans-unit id="8635324470284879211" datatype="html">
@ -7899,7 +7899,7 @@
<target state="translated">Ouvrir Duck.ai</target>
<context-group purpose="location">
<context context-type="sourcefile">apps/client/src/app/pages/portfolio/analysis/analysis-page.component.ts</context>
<context context-type="linenumber">223</context>
<context context-type="linenumber">206</context>
</context-group>
</trans-unit>
<trans-unit id="5674286808255988565" datatype="html">

60
apps/client/src/locales/messages.it.xlf

@ -103,7 +103,7 @@
<target state="translated">Vuoi davvero revocare l’accesso concesso?</target>
<context-group purpose="location">
<context context-type="sourcefile">apps/client/src/app/components/access-table/access-table.component.ts</context>
<context context-type="linenumber">121</context>
<context context-type="linenumber">126</context>
</context-group>
</trans-unit>
<trans-unit id="8953033926734869941" datatype="html">
@ -443,7 +443,7 @@
<target state="new">Splits</target>
<context-group purpose="location">
<context context-type="sourcefile">apps/client/src/app/components/admin-market-data/asset-profile-dialog/asset-profile-dialog.html</context>
<context context-type="linenumber">520</context>
<context context-type="linenumber">518</context>
</context-group>
</trans-unit>
<trans-unit id="267346373699222750" datatype="html">
@ -483,11 +483,11 @@
<target state="translated">Data</target>
<context-group purpose="location">
<context context-type="sourcefile">apps/client/src/app/components/admin-market-data/asset-profile-dialog/asset-profile-dialog.html</context>
<context context-type="linenumber">529</context>
<context context-type="linenumber">527</context>
</context-group>
<context-group purpose="location">
<context context-type="sourcefile">apps/client/src/app/components/admin-market-data/asset-profile-dialog/asset-profile-dialog.html</context>
<context context-type="linenumber">568</context>
<context context-type="linenumber">566</context>
</context-group>
<context-group purpose="location">
<context context-type="sourcefile">apps/client/src/app/pages/portfolio/activities/create-or-update-activity-dialog/create-or-update-activity-dialog.html</context>
@ -659,7 +659,7 @@
<target state="translated">Aggiungi</target>
<context-group purpose="location">
<context context-type="sourcefile">apps/client/src/app/components/admin-market-data/asset-profile-dialog/asset-profile-dialog.html</context>
<context context-type="linenumber">623</context>
<context context-type="linenumber">621</context>
</context-group>
<context-group purpose="location">
<context context-type="sourcefile">apps/client/src/app/components/admin-overview/admin-overview.html</context>
@ -851,7 +851,7 @@
<target state="new">Split Ratio</target>
<context-group purpose="location">
<context context-type="sourcefile">apps/client/src/app/components/admin-market-data/asset-profile-dialog/asset-profile-dialog.html</context>
<context context-type="linenumber">534</context>
<context context-type="linenumber">532</context>
</context-group>
</trans-unit>
<trans-unit id="103717583691217276" datatype="html">
@ -1383,7 +1383,7 @@
<target state="translated">Locale</target>
<context-group purpose="location">
<context context-type="sourcefile">apps/client/src/app/components/admin-market-data/asset-profile-dialog/asset-profile-dialog.html</context>
<context context-type="linenumber">674</context>
<context context-type="linenumber">672</context>
</context-group>
<context-group purpose="location">
<context context-type="sourcefile">apps/client/src/app/components/user-account-settings/user-account-settings.html</context>
@ -2767,7 +2767,7 @@
<target state="translated">Mensile</target>
<context-group purpose="location">
<context context-type="sourcefile">apps/client/src/app/pages/portfolio/analysis/analysis-page.component.ts</context>
<context context-type="linenumber">103</context>
<context context-type="linenumber">98</context>
</context-group>
</trans-unit>
<trans-unit id="6130372166370766747" datatype="html">
@ -3355,7 +3355,7 @@
</context-group>
<context-group purpose="location">
<context context-type="sourcefile">apps/client/src/app/pages/portfolio/analysis/analysis-page.component.ts</context>
<context context-type="linenumber">93</context>
<context context-type="linenumber">88</context>
</context-group>
</trans-unit>
<trans-unit id="5957846001261659229" datatype="html">
@ -3467,7 +3467,7 @@
</context-group>
<context-group purpose="location">
<context context-type="sourcefile">apps/client/src/app/pages/portfolio/analysis/analysis-page.component.ts</context>
<context context-type="linenumber">89</context>
<context context-type="linenumber">85</context>
</context-group>
<context-group purpose="location">
<context context-type="sourcefile">libs/ui/src/lib/i18n.ts</context>
@ -3611,7 +3611,7 @@
<target state="translated">Annuale</target>
<context-group purpose="location">
<context context-type="sourcefile">apps/client/src/app/pages/portfolio/analysis/analysis-page.component.ts</context>
<context context-type="linenumber">104</context>
<context context-type="linenumber">99</context>
</context-group>
</trans-unit>
<trans-unit id="72640258012696878" datatype="html">
@ -4211,7 +4211,7 @@
</context-group>
<context-group purpose="location">
<context context-type="sourcefile">apps/client/src/app/components/admin-market-data/asset-profile-dialog/asset-profile-dialog.html</context>
<context context-type="linenumber">715</context>
<context context-type="linenumber">713</context>
</context-group>
<context-group purpose="location">
<context context-type="sourcefile">apps/client/src/app/components/admin-platform/admin-platform.component.html</context>
@ -4363,7 +4363,7 @@
<target state="new">Shares After</target>
<context-group purpose="location">
<context context-type="sourcefile">apps/client/src/app/components/admin-market-data/asset-profile-dialog/asset-profile-dialog.html</context>
<context context-type="linenumber">592</context>
<context context-type="linenumber">590</context>
</context-group>
</trans-unit>
<trans-unit id="5393065248443550644" datatype="html">
@ -4635,7 +4635,7 @@
<target state="translated">Configurazione dello scraper</target>
<context-group purpose="location">
<context context-type="sourcefile">apps/client/src/app/components/admin-market-data/asset-profile-dialog/asset-profile-dialog.html</context>
<context context-type="linenumber">636</context>
<context context-type="linenumber">634</context>
</context-group>
</trans-unit>
<trans-unit id="7504169991280318133" datatype="html">
@ -6157,7 +6157,7 @@
<target state="translated">Ops! Impossibile elaborare i dati storici.</target>
<context-group purpose="location">
<context context-type="sourcefile">libs/ui/src/lib/historical-market-data-editor/historical-market-data-editor.component.ts</context>
<context context-type="linenumber">284</context>
<context context-type="linenumber">286</context>
</context-group>
</trans-unit>
<trans-unit id="7194017842579795231" datatype="html">
@ -6249,7 +6249,7 @@
<target state="translated">Prova</target>
<context-group purpose="location">
<context context-type="sourcefile">apps/client/src/app/components/admin-market-data/asset-profile-dialog/asset-profile-dialog.html</context>
<context context-type="linenumber">733</context>
<context context-type="linenumber">731</context>
</context-group>
</trans-unit>
<trans-unit id="6483680626836794824" datatype="html">
@ -6325,7 +6325,7 @@
<target state="translated">Investimento</target>
<context-group purpose="location">
<context context-type="sourcefile">apps/client/src/app/pages/portfolio/analysis/analysis-page.component.ts</context>
<context context-type="linenumber">109</context>
<context context-type="linenumber">104</context>
</context-group>
<context-group purpose="location">
<context context-type="sourcefile">apps/client/src/app/pages/resources/personal-finance-tools/product-page.component.ts</context>
@ -6517,7 +6517,7 @@
<target state="translated">Raccolta Dati</target>
<context-group purpose="location">
<context context-type="sourcefile">apps/client/src/app/components/admin-market-data/asset-profile-dialog/asset-profile-dialog.html</context>
<context context-type="linenumber">756</context>
<context context-type="linenumber">754</context>
</context-group>
<context-group purpose="location">
<context context-type="sourcefile">apps/client/src/app/components/admin-overview/admin-overview.html</context>
@ -7082,7 +7082,7 @@
</context-group>
<context-group purpose="location">
<context context-type="sourcefile">apps/client/src/app/components/admin-market-data/asset-profile-dialog/asset-profile-dialog.html</context>
<context context-type="linenumber">761</context>
<context context-type="linenumber">759</context>
</context-group>
<context-group purpose="location">
<context context-type="sourcefile">apps/client/src/app/components/admin-market-data/create-asset-profile-dialog/create-asset-profile-dialog.html</context>
@ -7134,7 +7134,7 @@
<target state="translated">Chiudi</target>
<context-group purpose="location">
<context context-type="sourcefile">apps/client/src/app/components/admin-market-data/asset-profile-dialog/asset-profile-dialog.html</context>
<context context-type="linenumber">763</context>
<context context-type="linenumber">761</context>
</context-group>
<context-group purpose="location">
<context context-type="sourcefile">apps/client/src/app/components/admin-market-data/create-asset-profile-dialog/create-asset-profile-dialog.html</context>
@ -7422,7 +7422,7 @@
<target state="new">Shares Before</target>
<context-group purpose="location">
<context context-type="sourcefile">apps/client/src/app/components/admin-market-data/asset-profile-dialog/asset-profile-dialog.html</context>
<context context-type="linenumber">605</context>
<context context-type="linenumber">603</context>
</context-group>
</trans-unit>
<trans-unit id="4605555143173907624" datatype="html">
@ -7712,7 +7712,7 @@
<target state="translated">Salva</target>
<context-group purpose="location">
<context context-type="sourcefile">apps/client/src/app/components/admin-market-data/asset-profile-dialog/asset-profile-dialog.html</context>
<context context-type="linenumber">772</context>
<context context-type="linenumber">770</context>
</context-group>
<context-group purpose="location">
<context context-type="sourcefile">apps/client/src/app/components/admin-market-data/create-asset-profile-dialog/create-asset-profile-dialog.html</context>
@ -7764,7 +7764,7 @@
</context-group>
<context-group purpose="location">
<context context-type="sourcefile">apps/client/src/app/components/user-account-access/user-account-access.component.ts</context>
<context context-type="linenumber">255</context>
<context context-type="linenumber">257</context>
</context-group>
</trans-unit>
<trans-unit id="1769610706135259386" datatype="html">
@ -7812,7 +7812,7 @@
<target state="translated">L’AI prompt è stato copiato negli appunti</target>
<context-group purpose="location">
<context context-type="sourcefile">apps/client/src/app/pages/portfolio/analysis/analysis-page.component.ts</context>
<context context-type="linenumber">222</context>
<context context-type="linenumber">205</context>
</context-group>
</trans-unit>
<trans-unit id="1616747898909934803" datatype="html">
@ -7820,7 +7820,7 @@
<target state="translated">Il link è stato copiato negli appunti</target>
<context-group purpose="location">
<context context-type="sourcefile">apps/client/src/app/components/access-table/access-table.component.ts</context>
<context context-type="linenumber">107</context>
<context context-type="linenumber">112</context>
</context-group>
</trans-unit>
<trans-unit id="8540986733881734625" datatype="html">
@ -7844,7 +7844,7 @@
<target state="translated">Prezzo di mercato predefinito</target>
<context-group purpose="location">
<context context-type="sourcefile">apps/client/src/app/components/admin-market-data/asset-profile-dialog/asset-profile-dialog.html</context>
<context context-type="linenumber">646</context>
<context context-type="linenumber">644</context>
</context-group>
</trans-unit>
<trans-unit id="1713271461473302108" datatype="html">
@ -7852,7 +7852,7 @@
<target state="translated">Modalità</target>
<context-group purpose="location">
<context context-type="sourcefile">apps/client/src/app/components/admin-market-data/asset-profile-dialog/asset-profile-dialog.html</context>
<context context-type="linenumber">683</context>
<context context-type="linenumber">681</context>
</context-group>
</trans-unit>
<trans-unit id="1717822775546188057" datatype="html">
@ -7868,7 +7868,7 @@
<target state="translated">Selettore</target>
<context-group purpose="location">
<context context-type="sourcefile">apps/client/src/app/components/admin-market-data/asset-profile-dialog/asset-profile-dialog.html</context>
<context context-type="linenumber">699</context>
<context context-type="linenumber">697</context>
</context-group>
</trans-unit>
<trans-unit id="645724892732039501" datatype="html">
@ -7876,7 +7876,7 @@
<target state="translated">Intestazioni della richiesta HTTP</target>
<context-group purpose="location">
<context context-type="sourcefile">apps/client/src/app/components/admin-market-data/asset-profile-dialog/asset-profile-dialog.html</context>
<context context-type="linenumber">659</context>
<context context-type="linenumber">657</context>
</context-group>
</trans-unit>
<trans-unit id="8635324470284879211" datatype="html">
@ -7900,7 +7900,7 @@
<target state="translated">Apri Duck.ai</target>
<context-group purpose="location">
<context context-type="sourcefile">apps/client/src/app/pages/portfolio/analysis/analysis-page.component.ts</context>
<context context-type="linenumber">223</context>
<context context-type="linenumber">206</context>
</context-group>
</trans-unit>
<trans-unit id="5674286808255988565" datatype="html">

60
apps/client/src/locales/messages.ja.xlf

@ -304,7 +304,7 @@
<target state="translated">付与したアクセス権を取り消してもよろしいですか?</target>
<context-group purpose="location">
<context context-type="sourcefile">apps/client/src/app/components/access-table/access-table.component.ts</context>
<context context-type="linenumber">121</context>
<context context-type="linenumber">126</context>
</context-group>
</trans-unit>
<trans-unit id="4029903570030679337" datatype="html">
@ -732,7 +732,7 @@
<target state="new">Splits</target>
<context-group purpose="location">
<context context-type="sourcefile">apps/client/src/app/components/admin-market-data/asset-profile-dialog/asset-profile-dialog.html</context>
<context context-type="linenumber">520</context>
<context context-type="linenumber">518</context>
</context-group>
</trans-unit>
<trans-unit id="267346373699222750" datatype="html">
@ -772,11 +772,11 @@
<target state="translated">日付</target>
<context-group purpose="location">
<context context-type="sourcefile">apps/client/src/app/components/admin-market-data/asset-profile-dialog/asset-profile-dialog.html</context>
<context context-type="linenumber">529</context>
<context context-type="linenumber">527</context>
</context-group>
<context-group purpose="location">
<context context-type="sourcefile">apps/client/src/app/components/admin-market-data/asset-profile-dialog/asset-profile-dialog.html</context>
<context context-type="linenumber">568</context>
<context context-type="linenumber">566</context>
</context-group>
<context-group purpose="location">
<context context-type="sourcefile">apps/client/src/app/pages/portfolio/activities/create-or-update-activity-dialog/create-or-update-activity-dialog.html</context>
@ -996,7 +996,7 @@
<target state="translated">エラー。過去データを解析できませんでした。</target>
<context-group purpose="location">
<context context-type="sourcefile">libs/ui/src/lib/historical-market-data-editor/historical-market-data-editor.component.ts</context>
<context context-type="linenumber">284</context>
<context context-type="linenumber">286</context>
</context-group>
</trans-unit>
<trans-unit id="7194017842579795231" datatype="html">
@ -1160,7 +1160,7 @@
<target state="translated">スクレイパーの設定</target>
<context-group purpose="location">
<context context-type="sourcefile">apps/client/src/app/components/admin-market-data/asset-profile-dialog/asset-profile-dialog.html</context>
<context context-type="linenumber">636</context>
<context context-type="linenumber">634</context>
</context-group>
</trans-unit>
<trans-unit id="4388879716045736175" datatype="html">
@ -1356,7 +1356,7 @@
<target state="translated">追加</target>
<context-group purpose="location">
<context context-type="sourcefile">apps/client/src/app/components/admin-market-data/asset-profile-dialog/asset-profile-dialog.html</context>
<context context-type="linenumber">623</context>
<context context-type="linenumber">621</context>
</context-group>
<context-group purpose="location">
<context context-type="sourcefile">apps/client/src/app/components/admin-overview/admin-overview.html</context>
@ -1400,7 +1400,7 @@
</context-group>
<context-group purpose="location">
<context context-type="sourcefile">apps/client/src/app/components/admin-market-data/asset-profile-dialog/asset-profile-dialog.html</context>
<context context-type="linenumber">715</context>
<context context-type="linenumber">713</context>
</context-group>
<context-group purpose="location">
<context context-type="sourcefile">apps/client/src/app/components/admin-platform/admin-platform.component.html</context>
@ -1724,7 +1724,7 @@
<target state="new">Split Ratio</target>
<context-group purpose="location">
<context context-type="sourcefile">apps/client/src/app/components/admin-market-data/asset-profile-dialog/asset-profile-dialog.html</context>
<context context-type="linenumber">534</context>
<context context-type="linenumber">532</context>
</context-group>
</trans-unit>
<trans-unit id="5486880308148746399" datatype="html">
@ -2556,7 +2556,7 @@
<target state="translated">ロケール</target>
<context-group purpose="location">
<context context-type="sourcefile">apps/client/src/app/components/admin-market-data/asset-profile-dialog/asset-profile-dialog.html</context>
<context context-type="linenumber">674</context>
<context context-type="linenumber">672</context>
</context-group>
<context-group purpose="location">
<context context-type="sourcefile">apps/client/src/app/components/user-account-settings/user-account-settings.html</context>
@ -4264,7 +4264,7 @@
<target state="new">Shares After</target>
<context-group purpose="location">
<context context-type="sourcefile">apps/client/src/app/components/admin-market-data/asset-profile-dialog/asset-profile-dialog.html</context>
<context context-type="linenumber">592</context>
<context context-type="linenumber">590</context>
</context-group>
</trans-unit>
<trans-unit id="8890553633144307762" datatype="html">
@ -4516,7 +4516,7 @@
</context-group>
<context-group purpose="location">
<context context-type="sourcefile">apps/client/src/app/pages/portfolio/analysis/analysis-page.component.ts</context>
<context context-type="linenumber">89</context>
<context context-type="linenumber">85</context>
</context-group>
<context-group purpose="location">
<context context-type="sourcefile">libs/ui/src/lib/i18n.ts</context>
@ -4536,7 +4536,7 @@
<target state="translated">毎月</target>
<context-group purpose="location">
<context context-type="sourcefile">apps/client/src/app/pages/portfolio/analysis/analysis-page.component.ts</context>
<context context-type="linenumber">103</context>
<context context-type="linenumber">98</context>
</context-group>
</trans-unit>
<trans-unit id="8036977202721714375" datatype="html">
@ -4544,7 +4544,7 @@
<target state="translated">年次</target>
<context-group purpose="location">
<context context-type="sourcefile">apps/client/src/app/pages/portfolio/analysis/analysis-page.component.ts</context>
<context context-type="linenumber">104</context>
<context context-type="linenumber">99</context>
</context-group>
</trans-unit>
<trans-unit id="6293970137138896363" datatype="html">
@ -6169,7 +6169,7 @@
</context-group>
<context-group purpose="location">
<context context-type="sourcefile">apps/client/src/app/pages/portfolio/analysis/analysis-page.component.ts</context>
<context context-type="linenumber">93</context>
<context context-type="linenumber">88</context>
</context-group>
</trans-unit>
<trans-unit id="5957846001261659229" datatype="html">
@ -6281,7 +6281,7 @@
<target state="translated">テスト</target>
<context-group purpose="location">
<context context-type="sourcefile">apps/client/src/app/components/admin-market-data/asset-profile-dialog/asset-profile-dialog.html</context>
<context context-type="linenumber">733</context>
<context context-type="linenumber">731</context>
</context-group>
</trans-unit>
<trans-unit id="2570446216260149991" datatype="html">
@ -6373,7 +6373,7 @@
<target state="translated">投資</target>
<context-group purpose="location">
<context context-type="sourcefile">apps/client/src/app/pages/portfolio/analysis/analysis-page.component.ts</context>
<context context-type="linenumber">109</context>
<context context-type="linenumber">104</context>
</context-group>
<context-group purpose="location">
<context context-type="sourcefile">apps/client/src/app/pages/resources/personal-finance-tools/product-page.component.ts</context>
@ -6566,7 +6566,7 @@
<target state="translated">データ収集</target>
<context-group purpose="location">
<context context-type="sourcefile">apps/client/src/app/components/admin-market-data/asset-profile-dialog/asset-profile-dialog.html</context>
<context context-type="linenumber">756</context>
<context context-type="linenumber">754</context>
</context-group>
<context-group purpose="location">
<context context-type="sourcefile">apps/client/src/app/components/admin-overview/admin-overview.html</context>
@ -7074,7 +7074,7 @@
</context-group>
<context-group purpose="location">
<context context-type="sourcefile">apps/client/src/app/components/admin-market-data/asset-profile-dialog/asset-profile-dialog.html</context>
<context context-type="linenumber">761</context>
<context context-type="linenumber">759</context>
</context-group>
<context-group purpose="location">
<context context-type="sourcefile">apps/client/src/app/components/admin-market-data/create-asset-profile-dialog/create-asset-profile-dialog.html</context>
@ -7150,7 +7150,7 @@
<target state="new">Close</target>
<context-group purpose="location">
<context context-type="sourcefile">apps/client/src/app/components/admin-market-data/asset-profile-dialog/asset-profile-dialog.html</context>
<context context-type="linenumber">763</context>
<context context-type="linenumber">761</context>
</context-group>
<context-group purpose="location">
<context context-type="sourcefile">apps/client/src/app/components/admin-market-data/create-asset-profile-dialog/create-asset-profile-dialog.html</context>
@ -7446,7 +7446,7 @@
<target state="new">Shares Before</target>
<context-group purpose="location">
<context context-type="sourcefile">apps/client/src/app/components/admin-market-data/asset-profile-dialog/asset-profile-dialog.html</context>
<context context-type="linenumber">605</context>
<context context-type="linenumber">603</context>
</context-group>
</trans-unit>
<trans-unit id="4605555143173907624" datatype="html">
@ -7736,7 +7736,7 @@
<target state="translated">保存</target>
<context-group purpose="location">
<context context-type="sourcefile">apps/client/src/app/components/admin-market-data/asset-profile-dialog/asset-profile-dialog.html</context>
<context context-type="linenumber">772</context>
<context context-type="linenumber">770</context>
</context-group>
<context-group purpose="location">
<context context-type="sourcefile">apps/client/src/app/components/admin-market-data/create-asset-profile-dialog/create-asset-profile-dialog.html</context>
@ -7804,7 +7804,7 @@
</context-group>
<context-group purpose="location">
<context context-type="sourcefile">apps/client/src/app/components/user-account-access/user-account-access.component.ts</context>
<context context-type="linenumber">255</context>
<context context-type="linenumber">257</context>
</context-group>
</trans-unit>
<trans-unit id="4060547242431613838" datatype="html">
@ -7836,7 +7836,7 @@
<target state="new">AI prompt has been copied to the clipboard</target>
<context-group purpose="location">
<context context-type="sourcefile">apps/client/src/app/pages/portfolio/analysis/analysis-page.component.ts</context>
<context context-type="linenumber">222</context>
<context context-type="linenumber">205</context>
</context-group>
</trans-unit>
<trans-unit id="1616747898909934803" datatype="html">
@ -7844,7 +7844,7 @@
<target state="new">Link has been copied to the clipboard</target>
<context-group purpose="location">
<context context-type="sourcefile">apps/client/src/app/components/access-table/access-table.component.ts</context>
<context context-type="linenumber">107</context>
<context context-type="linenumber">112</context>
</context-group>
</trans-unit>
<trans-unit id="1713271461473302108" datatype="html">
@ -7852,7 +7852,7 @@
<target state="new">Mode</target>
<context-group purpose="location">
<context context-type="sourcefile">apps/client/src/app/components/admin-market-data/asset-profile-dialog/asset-profile-dialog.html</context>
<context context-type="linenumber">683</context>
<context context-type="linenumber">681</context>
</context-group>
</trans-unit>
<trans-unit id="1717822775546188057" datatype="html">
@ -7868,7 +7868,7 @@
<target state="new">Default Market Price</target>
<context-group purpose="location">
<context context-type="sourcefile">apps/client/src/app/components/admin-market-data/asset-profile-dialog/asset-profile-dialog.html</context>
<context context-type="linenumber">646</context>
<context context-type="linenumber">644</context>
</context-group>
</trans-unit>
<trans-unit id="3540108566782816830" datatype="html">
@ -7876,7 +7876,7 @@
<target state="new">Selector</target>
<context-group purpose="location">
<context context-type="sourcefile">apps/client/src/app/components/admin-market-data/asset-profile-dialog/asset-profile-dialog.html</context>
<context context-type="linenumber">699</context>
<context context-type="linenumber">697</context>
</context-group>
</trans-unit>
<trans-unit id="6882618704933649036" datatype="html">
@ -7900,7 +7900,7 @@
<target state="new">HTTP Request Headers</target>
<context-group purpose="location">
<context context-type="sourcefile">apps/client/src/app/components/admin-market-data/asset-profile-dialog/asset-profile-dialog.html</context>
<context context-type="linenumber">659</context>
<context context-type="linenumber">657</context>
</context-group>
</trans-unit>
<trans-unit id="4547068148181074902" datatype="html">
@ -7924,7 +7924,7 @@
<target state="new">Open Duck.ai</target>
<context-group purpose="location">
<context context-type="sourcefile">apps/client/src/app/pages/portfolio/analysis/analysis-page.component.ts</context>
<context context-type="linenumber">223</context>
<context context-type="linenumber">206</context>
</context-group>
</trans-unit>
<trans-unit id="5674286808255988565" datatype="html">

60
apps/client/src/locales/messages.ko.xlf

@ -304,7 +304,7 @@
<target state="translated">이 부여된 접근 권한을 정말로 회수하시겠습니까?</target>
<context-group purpose="location">
<context context-type="sourcefile">apps/client/src/app/components/access-table/access-table.component.ts</context>
<context context-type="linenumber">121</context>
<context context-type="linenumber">126</context>
</context-group>
</trans-unit>
<trans-unit id="4029903570030679337" datatype="html">
@ -732,7 +732,7 @@
<target state="new">Splits</target>
<context-group purpose="location">
<context context-type="sourcefile">apps/client/src/app/components/admin-market-data/asset-profile-dialog/asset-profile-dialog.html</context>
<context context-type="linenumber">520</context>
<context context-type="linenumber">518</context>
</context-group>
</trans-unit>
<trans-unit id="267346373699222750" datatype="html">
@ -772,11 +772,11 @@
<target state="translated">날짜</target>
<context-group purpose="location">
<context context-type="sourcefile">apps/client/src/app/components/admin-market-data/asset-profile-dialog/asset-profile-dialog.html</context>
<context context-type="linenumber">529</context>
<context context-type="linenumber">527</context>
</context-group>
<context-group purpose="location">
<context context-type="sourcefile">apps/client/src/app/components/admin-market-data/asset-profile-dialog/asset-profile-dialog.html</context>
<context context-type="linenumber">568</context>
<context context-type="linenumber">566</context>
</context-group>
<context-group purpose="location">
<context context-type="sourcefile">apps/client/src/app/pages/portfolio/activities/create-or-update-activity-dialog/create-or-update-activity-dialog.html</context>
@ -996,7 +996,7 @@
<target state="translated">이런! 과거 데이터를 파싱할 수 없습니다.</target>
<context-group purpose="location">
<context context-type="sourcefile">libs/ui/src/lib/historical-market-data-editor/historical-market-data-editor.component.ts</context>
<context context-type="linenumber">284</context>
<context context-type="linenumber">286</context>
</context-group>
</trans-unit>
<trans-unit id="7194017842579795231" datatype="html">
@ -1160,7 +1160,7 @@
<target state="translated">스크래퍼 설정</target>
<context-group purpose="location">
<context context-type="sourcefile">apps/client/src/app/components/admin-market-data/asset-profile-dialog/asset-profile-dialog.html</context>
<context context-type="linenumber">636</context>
<context context-type="linenumber">634</context>
</context-group>
</trans-unit>
<trans-unit id="4388879716045736175" datatype="html">
@ -1356,7 +1356,7 @@
<target state="translated">추가</target>
<context-group purpose="location">
<context context-type="sourcefile">apps/client/src/app/components/admin-market-data/asset-profile-dialog/asset-profile-dialog.html</context>
<context context-type="linenumber">623</context>
<context context-type="linenumber">621</context>
</context-group>
<context-group purpose="location">
<context context-type="sourcefile">apps/client/src/app/components/admin-overview/admin-overview.html</context>
@ -1400,7 +1400,7 @@
</context-group>
<context-group purpose="location">
<context context-type="sourcefile">apps/client/src/app/components/admin-market-data/asset-profile-dialog/asset-profile-dialog.html</context>
<context context-type="linenumber">715</context>
<context context-type="linenumber">713</context>
</context-group>
<context-group purpose="location">
<context context-type="sourcefile">apps/client/src/app/components/admin-platform/admin-platform.component.html</context>
@ -1724,7 +1724,7 @@
<target state="new">Split Ratio</target>
<context-group purpose="location">
<context context-type="sourcefile">apps/client/src/app/components/admin-market-data/asset-profile-dialog/asset-profile-dialog.html</context>
<context context-type="linenumber">534</context>
<context context-type="linenumber">532</context>
</context-group>
</trans-unit>
<trans-unit id="5486880308148746399" datatype="html">
@ -2556,7 +2556,7 @@
<target state="translated">장소</target>
<context-group purpose="location">
<context context-type="sourcefile">apps/client/src/app/components/admin-market-data/asset-profile-dialog/asset-profile-dialog.html</context>
<context context-type="linenumber">674</context>
<context context-type="linenumber">672</context>
</context-group>
<context-group purpose="location">
<context context-type="sourcefile">apps/client/src/app/components/user-account-settings/user-account-settings.html</context>
@ -4256,7 +4256,7 @@
<target state="new">Shares After</target>
<context-group purpose="location">
<context context-type="sourcefile">apps/client/src/app/components/admin-market-data/asset-profile-dialog/asset-profile-dialog.html</context>
<context context-type="linenumber">592</context>
<context context-type="linenumber">590</context>
</context-group>
</trans-unit>
<trans-unit id="8890553633144307762" datatype="html">
@ -4508,7 +4508,7 @@
</context-group>
<context-group purpose="location">
<context context-type="sourcefile">apps/client/src/app/pages/portfolio/analysis/analysis-page.component.ts</context>
<context context-type="linenumber">89</context>
<context context-type="linenumber">85</context>
</context-group>
<context-group purpose="location">
<context context-type="sourcefile">libs/ui/src/lib/i18n.ts</context>
@ -4528,7 +4528,7 @@
<target state="translated">월간</target>
<context-group purpose="location">
<context context-type="sourcefile">apps/client/src/app/pages/portfolio/analysis/analysis-page.component.ts</context>
<context context-type="linenumber">103</context>
<context context-type="linenumber">98</context>
</context-group>
</trans-unit>
<trans-unit id="8036977202721714375" datatype="html">
@ -4536,7 +4536,7 @@
<target state="translated">매년</target>
<context-group purpose="location">
<context context-type="sourcefile">apps/client/src/app/pages/portfolio/analysis/analysis-page.component.ts</context>
<context context-type="linenumber">104</context>
<context context-type="linenumber">99</context>
</context-group>
</trans-unit>
<trans-unit id="6293970137138896363" datatype="html">
@ -6169,7 +6169,7 @@
</context-group>
<context-group purpose="location">
<context context-type="sourcefile">apps/client/src/app/pages/portfolio/analysis/analysis-page.component.ts</context>
<context context-type="linenumber">93</context>
<context context-type="linenumber">88</context>
</context-group>
</trans-unit>
<trans-unit id="5957846001261659229" datatype="html">
@ -6281,7 +6281,7 @@
<target state="translated">시험</target>
<context-group purpose="location">
<context context-type="sourcefile">apps/client/src/app/components/admin-market-data/asset-profile-dialog/asset-profile-dialog.html</context>
<context context-type="linenumber">733</context>
<context context-type="linenumber">731</context>
</context-group>
</trans-unit>
<trans-unit id="2570446216260149991" datatype="html">
@ -6373,7 +6373,7 @@
<target state="translated">투자</target>
<context-group purpose="location">
<context context-type="sourcefile">apps/client/src/app/pages/portfolio/analysis/analysis-page.component.ts</context>
<context context-type="linenumber">109</context>
<context context-type="linenumber">104</context>
</context-group>
<context-group purpose="location">
<context context-type="sourcefile">apps/client/src/app/pages/resources/personal-finance-tools/product-page.component.ts</context>
@ -6566,7 +6566,7 @@
<target state="translated">데이터 수집</target>
<context-group purpose="location">
<context context-type="sourcefile">apps/client/src/app/components/admin-market-data/asset-profile-dialog/asset-profile-dialog.html</context>
<context context-type="linenumber">756</context>
<context context-type="linenumber">754</context>
</context-group>
<context-group purpose="location">
<context context-type="sourcefile">apps/client/src/app/components/admin-overview/admin-overview.html</context>
@ -7074,7 +7074,7 @@
</context-group>
<context-group purpose="location">
<context context-type="sourcefile">apps/client/src/app/components/admin-market-data/asset-profile-dialog/asset-profile-dialog.html</context>
<context context-type="linenumber">761</context>
<context context-type="linenumber">759</context>
</context-group>
<context-group purpose="location">
<context context-type="sourcefile">apps/client/src/app/components/admin-market-data/create-asset-profile-dialog/create-asset-profile-dialog.html</context>
@ -7150,7 +7150,7 @@
<target state="translated">닫다</target>
<context-group purpose="location">
<context context-type="sourcefile">apps/client/src/app/components/admin-market-data/asset-profile-dialog/asset-profile-dialog.html</context>
<context context-type="linenumber">763</context>
<context context-type="linenumber">761</context>
</context-group>
<context-group purpose="location">
<context context-type="sourcefile">apps/client/src/app/components/admin-market-data/create-asset-profile-dialog/create-asset-profile-dialog.html</context>
@ -7446,7 +7446,7 @@
<target state="new">Shares Before</target>
<context-group purpose="location">
<context context-type="sourcefile">apps/client/src/app/components/admin-market-data/asset-profile-dialog/asset-profile-dialog.html</context>
<context context-type="linenumber">605</context>
<context context-type="linenumber">603</context>
</context-group>
</trans-unit>
<trans-unit id="4605555143173907624" datatype="html">
@ -7736,7 +7736,7 @@
<target state="translated">구하다</target>
<context-group purpose="location">
<context context-type="sourcefile">apps/client/src/app/components/admin-market-data/asset-profile-dialog/asset-profile-dialog.html</context>
<context context-type="linenumber">772</context>
<context context-type="linenumber">770</context>
</context-group>
<context-group purpose="location">
<context context-type="sourcefile">apps/client/src/app/components/admin-market-data/create-asset-profile-dialog/create-asset-profile-dialog.html</context>
@ -7804,7 +7804,7 @@
</context-group>
<context-group purpose="location">
<context context-type="sourcefile">apps/client/src/app/components/user-account-access/user-account-access.component.ts</context>
<context context-type="linenumber">255</context>
<context context-type="linenumber">257</context>
</context-group>
</trans-unit>
<trans-unit id="4060547242431613838" datatype="html">
@ -7836,7 +7836,7 @@
<target state="translated">AI 프롬프트가 클립보드에 복사되었습니다.</target>
<context-group purpose="location">
<context context-type="sourcefile">apps/client/src/app/pages/portfolio/analysis/analysis-page.component.ts</context>
<context context-type="linenumber">222</context>
<context context-type="linenumber">205</context>
</context-group>
</trans-unit>
<trans-unit id="1616747898909934803" datatype="html">
@ -7844,7 +7844,7 @@
<target state="translated">링크가 클립보드에 복사되었습니다.</target>
<context-group purpose="location">
<context context-type="sourcefile">apps/client/src/app/components/access-table/access-table.component.ts</context>
<context context-type="linenumber">107</context>
<context context-type="linenumber">112</context>
</context-group>
</trans-unit>
<trans-unit id="1713271461473302108" datatype="html">
@ -7852,7 +7852,7 @@
<target state="translated">방법</target>
<context-group purpose="location">
<context context-type="sourcefile">apps/client/src/app/components/admin-market-data/asset-profile-dialog/asset-profile-dialog.html</context>
<context context-type="linenumber">683</context>
<context context-type="linenumber">681</context>
</context-group>
</trans-unit>
<trans-unit id="1717822775546188057" datatype="html">
@ -7868,7 +7868,7 @@
<target state="translated">기본 시장 가격</target>
<context-group purpose="location">
<context context-type="sourcefile">apps/client/src/app/components/admin-market-data/asset-profile-dialog/asset-profile-dialog.html</context>
<context context-type="linenumber">646</context>
<context context-type="linenumber">644</context>
</context-group>
</trans-unit>
<trans-unit id="3540108566782816830" datatype="html">
@ -7876,7 +7876,7 @@
<target state="translated">선택자</target>
<context-group purpose="location">
<context context-type="sourcefile">apps/client/src/app/components/admin-market-data/asset-profile-dialog/asset-profile-dialog.html</context>
<context context-type="linenumber">699</context>
<context context-type="linenumber">697</context>
</context-group>
</trans-unit>
<trans-unit id="6882618704933649036" datatype="html">
@ -7900,7 +7900,7 @@
<target state="translated">HTTP 요청 헤더</target>
<context-group purpose="location">
<context context-type="sourcefile">apps/client/src/app/components/admin-market-data/asset-profile-dialog/asset-profile-dialog.html</context>
<context context-type="linenumber">659</context>
<context context-type="linenumber">657</context>
</context-group>
</trans-unit>
<trans-unit id="4547068148181074902" datatype="html">
@ -7924,7 +7924,7 @@
<target state="translated">오픈 Duck.ai</target>
<context-group purpose="location">
<context context-type="sourcefile">apps/client/src/app/pages/portfolio/analysis/analysis-page.component.ts</context>
<context context-type="linenumber">223</context>
<context context-type="linenumber">206</context>
</context-group>
</trans-unit>
<trans-unit id="5674286808255988565" datatype="html">

60
apps/client/src/locales/messages.nl.xlf

@ -102,7 +102,7 @@
<target state="translated">Wil je deze verleende toegang echt intrekken?</target>
<context-group purpose="location">
<context context-type="sourcefile">apps/client/src/app/components/access-table/access-table.component.ts</context>
<context context-type="linenumber">121</context>
<context context-type="linenumber">126</context>
</context-group>
</trans-unit>
<trans-unit id="8953033926734869941" datatype="html">
@ -442,7 +442,7 @@
<target state="new">Splits</target>
<context-group purpose="location">
<context context-type="sourcefile">apps/client/src/app/components/admin-market-data/asset-profile-dialog/asset-profile-dialog.html</context>
<context context-type="linenumber">520</context>
<context context-type="linenumber">518</context>
</context-group>
</trans-unit>
<trans-unit id="267346373699222750" datatype="html">
@ -482,11 +482,11 @@
<target state="translated">Datum</target>
<context-group purpose="location">
<context context-type="sourcefile">apps/client/src/app/components/admin-market-data/asset-profile-dialog/asset-profile-dialog.html</context>
<context context-type="linenumber">529</context>
<context context-type="linenumber">527</context>
</context-group>
<context-group purpose="location">
<context context-type="sourcefile">apps/client/src/app/components/admin-market-data/asset-profile-dialog/asset-profile-dialog.html</context>
<context context-type="linenumber">568</context>
<context context-type="linenumber">566</context>
</context-group>
<context-group purpose="location">
<context context-type="sourcefile">apps/client/src/app/pages/portfolio/activities/create-or-update-activity-dialog/create-or-update-activity-dialog.html</context>
@ -658,7 +658,7 @@
<target state="translated">Toevoegen</target>
<context-group purpose="location">
<context context-type="sourcefile">apps/client/src/app/components/admin-market-data/asset-profile-dialog/asset-profile-dialog.html</context>
<context context-type="linenumber">623</context>
<context context-type="linenumber">621</context>
</context-group>
<context-group purpose="location">
<context context-type="sourcefile">apps/client/src/app/components/admin-overview/admin-overview.html</context>
@ -850,7 +850,7 @@
<target state="new">Split Ratio</target>
<context-group purpose="location">
<context context-type="sourcefile">apps/client/src/app/components/admin-market-data/asset-profile-dialog/asset-profile-dialog.html</context>
<context context-type="linenumber">534</context>
<context context-type="linenumber">532</context>
</context-group>
</trans-unit>
<trans-unit id="103717583691217276" datatype="html">
@ -1382,7 +1382,7 @@
<target state="translated">Locatie</target>
<context-group purpose="location">
<context context-type="sourcefile">apps/client/src/app/components/admin-market-data/asset-profile-dialog/asset-profile-dialog.html</context>
<context context-type="linenumber">674</context>
<context context-type="linenumber">672</context>
</context-group>
<context-group purpose="location">
<context context-type="sourcefile">apps/client/src/app/components/user-account-settings/user-account-settings.html</context>
@ -2766,7 +2766,7 @@
<target state="translated">Maandelijks</target>
<context-group purpose="location">
<context context-type="sourcefile">apps/client/src/app/pages/portfolio/analysis/analysis-page.component.ts</context>
<context context-type="linenumber">103</context>
<context context-type="linenumber">98</context>
</context-group>
</trans-unit>
<trans-unit id="6130372166370766747" datatype="html">
@ -3354,7 +3354,7 @@
</context-group>
<context-group purpose="location">
<context context-type="sourcefile">apps/client/src/app/pages/portfolio/analysis/analysis-page.component.ts</context>
<context context-type="linenumber">93</context>
<context context-type="linenumber">88</context>
</context-group>
</trans-unit>
<trans-unit id="5957846001261659229" datatype="html">
@ -3466,7 +3466,7 @@
</context-group>
<context-group purpose="location">
<context context-type="sourcefile">apps/client/src/app/pages/portfolio/analysis/analysis-page.component.ts</context>
<context context-type="linenumber">89</context>
<context context-type="linenumber">85</context>
</context-group>
<context-group purpose="location">
<context context-type="sourcefile">libs/ui/src/lib/i18n.ts</context>
@ -3610,7 +3610,7 @@
<target state="translated">Jaarlijks</target>
<context-group purpose="location">
<context context-type="sourcefile">apps/client/src/app/pages/portfolio/analysis/analysis-page.component.ts</context>
<context context-type="linenumber">104</context>
<context context-type="linenumber">99</context>
</context-group>
</trans-unit>
<trans-unit id="72640258012696878" datatype="html">
@ -4210,7 +4210,7 @@
</context-group>
<context-group purpose="location">
<context context-type="sourcefile">apps/client/src/app/components/admin-market-data/asset-profile-dialog/asset-profile-dialog.html</context>
<context context-type="linenumber">715</context>
<context context-type="linenumber">713</context>
</context-group>
<context-group purpose="location">
<context context-type="sourcefile">apps/client/src/app/components/admin-platform/admin-platform.component.html</context>
@ -4362,7 +4362,7 @@
<target state="new">Shares After</target>
<context-group purpose="location">
<context context-type="sourcefile">apps/client/src/app/components/admin-market-data/asset-profile-dialog/asset-profile-dialog.html</context>
<context context-type="linenumber">592</context>
<context context-type="linenumber">590</context>
</context-group>
</trans-unit>
<trans-unit id="5393065248443550644" datatype="html">
@ -4634,7 +4634,7 @@
<target state="translated">Scraper instellingen</target>
<context-group purpose="location">
<context context-type="sourcefile">apps/client/src/app/components/admin-market-data/asset-profile-dialog/asset-profile-dialog.html</context>
<context context-type="linenumber">636</context>
<context context-type="linenumber">634</context>
</context-group>
</trans-unit>
<trans-unit id="7504169991280318133" datatype="html">
@ -6156,7 +6156,7 @@
<target state="translated">Oeps! Ophalen van historische data is mislukt.</target>
<context-group purpose="location">
<context context-type="sourcefile">libs/ui/src/lib/historical-market-data-editor/historical-market-data-editor.component.ts</context>
<context context-type="linenumber">284</context>
<context context-type="linenumber">286</context>
</context-group>
</trans-unit>
<trans-unit id="7194017842579795231" datatype="html">
@ -6248,7 +6248,7 @@
<target state="translated">Test</target>
<context-group purpose="location">
<context context-type="sourcefile">apps/client/src/app/components/admin-market-data/asset-profile-dialog/asset-profile-dialog.html</context>
<context context-type="linenumber">733</context>
<context context-type="linenumber">731</context>
</context-group>
</trans-unit>
<trans-unit id="6483680626836794824" datatype="html">
@ -6324,7 +6324,7 @@
<target state="translated">Investering</target>
<context-group purpose="location">
<context context-type="sourcefile">apps/client/src/app/pages/portfolio/analysis/analysis-page.component.ts</context>
<context context-type="linenumber">109</context>
<context context-type="linenumber">104</context>
</context-group>
<context-group purpose="location">
<context context-type="sourcefile">apps/client/src/app/pages/resources/personal-finance-tools/product-page.component.ts</context>
@ -6516,7 +6516,7 @@
<target state="translated">Data Verzamelen</target>
<context-group purpose="location">
<context context-type="sourcefile">apps/client/src/app/components/admin-market-data/asset-profile-dialog/asset-profile-dialog.html</context>
<context context-type="linenumber">756</context>
<context context-type="linenumber">754</context>
</context-group>
<context-group purpose="location">
<context context-type="sourcefile">apps/client/src/app/components/admin-overview/admin-overview.html</context>
@ -7081,7 +7081,7 @@
</context-group>
<context-group purpose="location">
<context context-type="sourcefile">apps/client/src/app/components/admin-market-data/asset-profile-dialog/asset-profile-dialog.html</context>
<context context-type="linenumber">761</context>
<context context-type="linenumber">759</context>
</context-group>
<context-group purpose="location">
<context context-type="sourcefile">apps/client/src/app/components/admin-market-data/create-asset-profile-dialog/create-asset-profile-dialog.html</context>
@ -7133,7 +7133,7 @@
<target state="translated">Sluiten</target>
<context-group purpose="location">
<context context-type="sourcefile">apps/client/src/app/components/admin-market-data/asset-profile-dialog/asset-profile-dialog.html</context>
<context context-type="linenumber">763</context>
<context context-type="linenumber">761</context>
</context-group>
<context-group purpose="location">
<context context-type="sourcefile">apps/client/src/app/components/admin-market-data/create-asset-profile-dialog/create-asset-profile-dialog.html</context>
@ -7421,7 +7421,7 @@
<target state="new">Shares Before</target>
<context-group purpose="location">
<context context-type="sourcefile">apps/client/src/app/components/admin-market-data/asset-profile-dialog/asset-profile-dialog.html</context>
<context context-type="linenumber">605</context>
<context context-type="linenumber">603</context>
</context-group>
</trans-unit>
<trans-unit id="4605555143173907624" datatype="html">
@ -7711,7 +7711,7 @@
<target state="translated">Opslaan</target>
<context-group purpose="location">
<context context-type="sourcefile">apps/client/src/app/components/admin-market-data/asset-profile-dialog/asset-profile-dialog.html</context>
<context context-type="linenumber">772</context>
<context context-type="linenumber">770</context>
</context-group>
<context-group purpose="location">
<context context-type="sourcefile">apps/client/src/app/components/admin-market-data/create-asset-profile-dialog/create-asset-profile-dialog.html</context>
@ -7763,7 +7763,7 @@
</context-group>
<context-group purpose="location">
<context context-type="sourcefile">apps/client/src/app/components/user-account-access/user-account-access.component.ts</context>
<context context-type="linenumber">255</context>
<context context-type="linenumber">257</context>
</context-group>
</trans-unit>
<trans-unit id="1769610706135259386" datatype="html">
@ -7811,7 +7811,7 @@
<target state="translated">AI-prompt is naar het klembord gekopieerd</target>
<context-group purpose="location">
<context context-type="sourcefile">apps/client/src/app/pages/portfolio/analysis/analysis-page.component.ts</context>
<context context-type="linenumber">222</context>
<context context-type="linenumber">205</context>
</context-group>
</trans-unit>
<trans-unit id="1616747898909934803" datatype="html">
@ -7819,7 +7819,7 @@
<target state="translated">Link is gekopieerd naar klemboord</target>
<context-group purpose="location">
<context context-type="sourcefile">apps/client/src/app/components/access-table/access-table.component.ts</context>
<context context-type="linenumber">107</context>
<context context-type="linenumber">112</context>
</context-group>
</trans-unit>
<trans-unit id="8540986733881734625" datatype="html">
@ -7843,7 +7843,7 @@
<target state="translated">Standaard Marktprijs</target>
<context-group purpose="location">
<context context-type="sourcefile">apps/client/src/app/components/admin-market-data/asset-profile-dialog/asset-profile-dialog.html</context>
<context context-type="linenumber">646</context>
<context context-type="linenumber">644</context>
</context-group>
</trans-unit>
<trans-unit id="1713271461473302108" datatype="html">
@ -7851,7 +7851,7 @@
<target state="translated">Modus</target>
<context-group purpose="location">
<context context-type="sourcefile">apps/client/src/app/components/admin-market-data/asset-profile-dialog/asset-profile-dialog.html</context>
<context context-type="linenumber">683</context>
<context context-type="linenumber">681</context>
</context-group>
</trans-unit>
<trans-unit id="1717822775546188057" datatype="html">
@ -7867,7 +7867,7 @@
<target state="translated">Kiezer</target>
<context-group purpose="location">
<context context-type="sourcefile">apps/client/src/app/components/admin-market-data/asset-profile-dialog/asset-profile-dialog.html</context>
<context context-type="linenumber">699</context>
<context context-type="linenumber">697</context>
</context-group>
</trans-unit>
<trans-unit id="645724892732039501" datatype="html">
@ -7875,7 +7875,7 @@
<target state="translated">HTTP Verzoek Headers</target>
<context-group purpose="location">
<context context-type="sourcefile">apps/client/src/app/components/admin-market-data/asset-profile-dialog/asset-profile-dialog.html</context>
<context context-type="linenumber">659</context>
<context context-type="linenumber">657</context>
</context-group>
</trans-unit>
<trans-unit id="8635324470284879211" datatype="html">
@ -7899,7 +7899,7 @@
<target state="translated">Open Duck.ai</target>
<context-group purpose="location">
<context context-type="sourcefile">apps/client/src/app/pages/portfolio/analysis/analysis-page.component.ts</context>
<context context-type="linenumber">223</context>
<context context-type="linenumber">206</context>
</context-group>
</trans-unit>
<trans-unit id="5674286808255988565" datatype="html">

60
apps/client/src/locales/messages.pl.xlf

@ -303,7 +303,7 @@
<target state="translated">Czy na pewno chcesz cofnąć przyznany dostęp?</target>
<context-group purpose="location">
<context context-type="sourcefile">apps/client/src/app/components/access-table/access-table.component.ts</context>
<context context-type="linenumber">121</context>
<context context-type="linenumber">126</context>
</context-group>
</trans-unit>
<trans-unit id="4029903570030679337" datatype="html">
@ -723,7 +723,7 @@
<target state="new">Splits</target>
<context-group purpose="location">
<context context-type="sourcefile">apps/client/src/app/components/admin-market-data/asset-profile-dialog/asset-profile-dialog.html</context>
<context context-type="linenumber">520</context>
<context context-type="linenumber">518</context>
</context-group>
</trans-unit>
<trans-unit id="267346373699222750" datatype="html">
@ -763,11 +763,11 @@
<target state="translated">Data</target>
<context-group purpose="location">
<context context-type="sourcefile">apps/client/src/app/components/admin-market-data/asset-profile-dialog/asset-profile-dialog.html</context>
<context context-type="linenumber">529</context>
<context context-type="linenumber">527</context>
</context-group>
<context-group purpose="location">
<context context-type="sourcefile">apps/client/src/app/components/admin-market-data/asset-profile-dialog/asset-profile-dialog.html</context>
<context context-type="linenumber">568</context>
<context context-type="linenumber">566</context>
</context-group>
<context-group purpose="location">
<context context-type="sourcefile">apps/client/src/app/pages/portfolio/activities/create-or-update-activity-dialog/create-or-update-activity-dialog.html</context>
@ -971,7 +971,7 @@
<target state="translated">Ups! Nie udało się sparsować danych historycznych.</target>
<context-group purpose="location">
<context context-type="sourcefile">libs/ui/src/lib/historical-market-data-editor/historical-market-data-editor.component.ts</context>
<context context-type="linenumber">284</context>
<context context-type="linenumber">286</context>
</context-group>
</trans-unit>
<trans-unit id="7194017842579795231" datatype="html">
@ -1127,7 +1127,7 @@
<target state="translated">Konfiguracja Scrapera</target>
<context-group purpose="location">
<context context-type="sourcefile">apps/client/src/app/components/admin-market-data/asset-profile-dialog/asset-profile-dialog.html</context>
<context context-type="linenumber">636</context>
<context context-type="linenumber">634</context>
</context-group>
</trans-unit>
<trans-unit id="4388879716045736175" datatype="html">
@ -1323,7 +1323,7 @@
<target state="translated">Dodaj</target>
<context-group purpose="location">
<context context-type="sourcefile">apps/client/src/app/components/admin-market-data/asset-profile-dialog/asset-profile-dialog.html</context>
<context context-type="linenumber">623</context>
<context context-type="linenumber">621</context>
</context-group>
<context-group purpose="location">
<context context-type="sourcefile">apps/client/src/app/components/admin-overview/admin-overview.html</context>
@ -1367,7 +1367,7 @@
</context-group>
<context-group purpose="location">
<context context-type="sourcefile">apps/client/src/app/components/admin-market-data/asset-profile-dialog/asset-profile-dialog.html</context>
<context context-type="linenumber">715</context>
<context context-type="linenumber">713</context>
</context-group>
<context-group purpose="location">
<context context-type="sourcefile">apps/client/src/app/components/admin-platform/admin-platform.component.html</context>
@ -1691,7 +1691,7 @@
<target state="new">Split Ratio</target>
<context-group purpose="location">
<context context-type="sourcefile">apps/client/src/app/components/admin-market-data/asset-profile-dialog/asset-profile-dialog.html</context>
<context context-type="linenumber">534</context>
<context context-type="linenumber">532</context>
</context-group>
</trans-unit>
<trans-unit id="5486880308148746399" datatype="html">
@ -2523,7 +2523,7 @@
<target state="translated">Ustawienia Regionalne</target>
<context-group purpose="location">
<context context-type="sourcefile">apps/client/src/app/components/admin-market-data/asset-profile-dialog/asset-profile-dialog.html</context>
<context context-type="linenumber">674</context>
<context context-type="linenumber">672</context>
</context-group>
<context-group purpose="location">
<context context-type="sourcefile">apps/client/src/app/components/user-account-settings/user-account-settings.html</context>
@ -4223,7 +4223,7 @@
<target state="new">Shares After</target>
<context-group purpose="location">
<context context-type="sourcefile">apps/client/src/app/components/admin-market-data/asset-profile-dialog/asset-profile-dialog.html</context>
<context context-type="linenumber">592</context>
<context context-type="linenumber">590</context>
</context-group>
</trans-unit>
<trans-unit id="8890553633144307762" datatype="html">
@ -4475,7 +4475,7 @@
</context-group>
<context-group purpose="location">
<context context-type="sourcefile">apps/client/src/app/pages/portfolio/analysis/analysis-page.component.ts</context>
<context context-type="linenumber">89</context>
<context context-type="linenumber">85</context>
</context-group>
<context-group purpose="location">
<context context-type="sourcefile">libs/ui/src/lib/i18n.ts</context>
@ -4495,7 +4495,7 @@
<target state="translated">Miesięcznie</target>
<context-group purpose="location">
<context context-type="sourcefile">apps/client/src/app/pages/portfolio/analysis/analysis-page.component.ts</context>
<context context-type="linenumber">103</context>
<context context-type="linenumber">98</context>
</context-group>
</trans-unit>
<trans-unit id="8036977202721714375" datatype="html">
@ -4503,7 +4503,7 @@
<target state="translated">Rocznie</target>
<context-group purpose="location">
<context context-type="sourcefile">apps/client/src/app/pages/portfolio/analysis/analysis-page.component.ts</context>
<context context-type="linenumber">104</context>
<context context-type="linenumber">99</context>
</context-group>
</trans-unit>
<trans-unit id="6293970137138896363" datatype="html">
@ -6092,7 +6092,7 @@
</context-group>
<context-group purpose="location">
<context context-type="sourcefile">apps/client/src/app/pages/portfolio/analysis/analysis-page.component.ts</context>
<context context-type="linenumber">93</context>
<context context-type="linenumber">88</context>
</context-group>
</trans-unit>
<trans-unit id="5957846001261659229" datatype="html">
@ -6248,7 +6248,7 @@
<target state="translated">Test</target>
<context-group purpose="location">
<context context-type="sourcefile">apps/client/src/app/components/admin-market-data/asset-profile-dialog/asset-profile-dialog.html</context>
<context context-type="linenumber">733</context>
<context context-type="linenumber">731</context>
</context-group>
</trans-unit>
<trans-unit id="6483680626836794824" datatype="html">
@ -6324,7 +6324,7 @@
<target state="translated">Inwestycje</target>
<context-group purpose="location">
<context context-type="sourcefile">apps/client/src/app/pages/portfolio/analysis/analysis-page.component.ts</context>
<context context-type="linenumber">109</context>
<context context-type="linenumber">104</context>
</context-group>
<context-group purpose="location">
<context context-type="sourcefile">apps/client/src/app/pages/resources/personal-finance-tools/product-page.component.ts</context>
@ -6516,7 +6516,7 @@
<target state="translated">Gromadzenie Danych</target>
<context-group purpose="location">
<context context-type="sourcefile">apps/client/src/app/components/admin-market-data/asset-profile-dialog/asset-profile-dialog.html</context>
<context context-type="linenumber">756</context>
<context context-type="linenumber">754</context>
</context-group>
<context-group purpose="location">
<context context-type="sourcefile">apps/client/src/app/components/admin-overview/admin-overview.html</context>
@ -7081,7 +7081,7 @@
</context-group>
<context-group purpose="location">
<context context-type="sourcefile">apps/client/src/app/components/admin-market-data/asset-profile-dialog/asset-profile-dialog.html</context>
<context context-type="linenumber">761</context>
<context context-type="linenumber">759</context>
</context-group>
<context-group purpose="location">
<context context-type="sourcefile">apps/client/src/app/components/admin-market-data/create-asset-profile-dialog/create-asset-profile-dialog.html</context>
@ -7133,7 +7133,7 @@
<target state="translated">Zamknij</target>
<context-group purpose="location">
<context context-type="sourcefile">apps/client/src/app/components/admin-market-data/asset-profile-dialog/asset-profile-dialog.html</context>
<context context-type="linenumber">763</context>
<context context-type="linenumber">761</context>
</context-group>
<context-group purpose="location">
<context context-type="sourcefile">apps/client/src/app/components/admin-market-data/create-asset-profile-dialog/create-asset-profile-dialog.html</context>
@ -7421,7 +7421,7 @@
<target state="new">Shares Before</target>
<context-group purpose="location">
<context context-type="sourcefile">apps/client/src/app/components/admin-market-data/asset-profile-dialog/asset-profile-dialog.html</context>
<context context-type="linenumber">605</context>
<context context-type="linenumber">603</context>
</context-group>
</trans-unit>
<trans-unit id="4605555143173907624" datatype="html">
@ -7711,7 +7711,7 @@
<target state="translated">Zapisz</target>
<context-group purpose="location">
<context context-type="sourcefile">apps/client/src/app/components/admin-market-data/asset-profile-dialog/asset-profile-dialog.html</context>
<context context-type="linenumber">772</context>
<context context-type="linenumber">770</context>
</context-group>
<context-group purpose="location">
<context context-type="sourcefile">apps/client/src/app/components/admin-market-data/create-asset-profile-dialog/create-asset-profile-dialog.html</context>
@ -7763,7 +7763,7 @@
</context-group>
<context-group purpose="location">
<context context-type="sourcefile">apps/client/src/app/components/user-account-access/user-account-access.component.ts</context>
<context context-type="linenumber">255</context>
<context context-type="linenumber">257</context>
</context-group>
</trans-unit>
<trans-unit id="1769610706135259386" datatype="html">
@ -7811,7 +7811,7 @@
<target state="translated">Prompt AI został skopiowany do schowka</target>
<context-group purpose="location">
<context context-type="sourcefile">apps/client/src/app/pages/portfolio/analysis/analysis-page.component.ts</context>
<context context-type="linenumber">222</context>
<context context-type="linenumber">205</context>
</context-group>
</trans-unit>
<trans-unit id="1616747898909934803" datatype="html">
@ -7819,7 +7819,7 @@
<target state="translated">Link został skopiowany do schowka</target>
<context-group purpose="location">
<context context-type="sourcefile">apps/client/src/app/components/access-table/access-table.component.ts</context>
<context context-type="linenumber">107</context>
<context context-type="linenumber">112</context>
</context-group>
</trans-unit>
<trans-unit id="8540986733881734625" datatype="html">
@ -7843,7 +7843,7 @@
<target state="translated">Domyślna cena rynkowa</target>
<context-group purpose="location">
<context context-type="sourcefile">apps/client/src/app/components/admin-market-data/asset-profile-dialog/asset-profile-dialog.html</context>
<context context-type="linenumber">646</context>
<context context-type="linenumber">644</context>
</context-group>
</trans-unit>
<trans-unit id="1713271461473302108" datatype="html">
@ -7851,7 +7851,7 @@
<target state="translated">Tryb</target>
<context-group purpose="location">
<context context-type="sourcefile">apps/client/src/app/components/admin-market-data/asset-profile-dialog/asset-profile-dialog.html</context>
<context context-type="linenumber">683</context>
<context context-type="linenumber">681</context>
</context-group>
</trans-unit>
<trans-unit id="1717822775546188057" datatype="html">
@ -7867,7 +7867,7 @@
<target state="translated">Selektor</target>
<context-group purpose="location">
<context context-type="sourcefile">apps/client/src/app/components/admin-market-data/asset-profile-dialog/asset-profile-dialog.html</context>
<context context-type="linenumber">699</context>
<context context-type="linenumber">697</context>
</context-group>
</trans-unit>
<trans-unit id="645724892732039501" datatype="html">
@ -7875,7 +7875,7 @@
<target state="translated">Nagłówki żądań HTTP</target>
<context-group purpose="location">
<context context-type="sourcefile">apps/client/src/app/components/admin-market-data/asset-profile-dialog/asset-profile-dialog.html</context>
<context context-type="linenumber">659</context>
<context context-type="linenumber">657</context>
</context-group>
</trans-unit>
<trans-unit id="8635324470284879211" datatype="html">
@ -7899,7 +7899,7 @@
<target state="translated">Otwórz Duck.ai</target>
<context-group purpose="location">
<context context-type="sourcefile">apps/client/src/app/pages/portfolio/analysis/analysis-page.component.ts</context>
<context context-type="linenumber">223</context>
<context context-type="linenumber">206</context>
</context-group>
</trans-unit>
<trans-unit id="5674286808255988565" datatype="html">

60
apps/client/src/locales/messages.pt.xlf

@ -94,7 +94,7 @@
<target state="translated">Pretende realmente revogar este acesso concedido?</target>
<context-group purpose="location">
<context context-type="sourcefile">apps/client/src/app/components/access-table/access-table.component.ts</context>
<context context-type="linenumber">121</context>
<context context-type="linenumber">126</context>
</context-group>
</trans-unit>
<trans-unit id="962765493084218985" datatype="html">
@ -506,7 +506,7 @@
<target state="new">Splits</target>
<context-group purpose="location">
<context context-type="sourcefile">apps/client/src/app/components/admin-market-data/asset-profile-dialog/asset-profile-dialog.html</context>
<context context-type="linenumber">520</context>
<context context-type="linenumber">518</context>
</context-group>
</trans-unit>
<trans-unit id="267346373699222750" datatype="html">
@ -546,11 +546,11 @@
<target state="translated">Data</target>
<context-group purpose="location">
<context context-type="sourcefile">apps/client/src/app/components/admin-market-data/asset-profile-dialog/asset-profile-dialog.html</context>
<context context-type="linenumber">529</context>
<context context-type="linenumber">527</context>
</context-group>
<context-group purpose="location">
<context context-type="sourcefile">apps/client/src/app/components/admin-market-data/asset-profile-dialog/asset-profile-dialog.html</context>
<context context-type="linenumber">568</context>
<context context-type="linenumber">566</context>
</context-group>
<context-group purpose="location">
<context context-type="sourcefile">apps/client/src/app/pages/portfolio/activities/create-or-update-activity-dialog/create-or-update-activity-dialog.html</context>
@ -762,7 +762,7 @@
<target state="translated">Adicionar</target>
<context-group purpose="location">
<context context-type="sourcefile">apps/client/src/app/components/admin-market-data/asset-profile-dialog/asset-profile-dialog.html</context>
<context context-type="linenumber">623</context>
<context context-type="linenumber">621</context>
</context-group>
<context-group purpose="location">
<context context-type="sourcefile">apps/client/src/app/components/admin-overview/admin-overview.html</context>
@ -966,7 +966,7 @@
<target state="new">Split Ratio</target>
<context-group purpose="location">
<context context-type="sourcefile">apps/client/src/app/components/admin-market-data/asset-profile-dialog/asset-profile-dialog.html</context>
<context context-type="linenumber">534</context>
<context context-type="linenumber">532</context>
</context-group>
</trans-unit>
<trans-unit id="5486880308148746399" datatype="html">
@ -1710,7 +1710,7 @@
<target state="translated">Localidade</target>
<context-group purpose="location">
<context context-type="sourcefile">apps/client/src/app/components/admin-market-data/asset-profile-dialog/asset-profile-dialog.html</context>
<context context-type="linenumber">674</context>
<context context-type="linenumber">672</context>
</context-group>
<context-group purpose="location">
<context context-type="sourcefile">apps/client/src/app/components/user-account-settings/user-account-settings.html</context>
@ -2590,7 +2590,7 @@
<target state="translated">Mensalmente</target>
<context-group purpose="location">
<context context-type="sourcefile">apps/client/src/app/pages/portfolio/analysis/analysis-page.component.ts</context>
<context context-type="linenumber">103</context>
<context context-type="linenumber">98</context>
</context-group>
</trans-unit>
<trans-unit id="6293970137138896363" datatype="html">
@ -3366,7 +3366,7 @@
</context-group>
<context-group purpose="location">
<context context-type="sourcefile">apps/client/src/app/pages/portfolio/analysis/analysis-page.component.ts</context>
<context context-type="linenumber">93</context>
<context context-type="linenumber">88</context>
</context-group>
</trans-unit>
<trans-unit id="5957846001261659229" datatype="html">
@ -3542,7 +3542,7 @@
</context-group>
<context-group purpose="location">
<context context-type="sourcefile">apps/client/src/app/pages/portfolio/analysis/analysis-page.component.ts</context>
<context context-type="linenumber">89</context>
<context context-type="linenumber">85</context>
</context-group>
<context-group purpose="location">
<context context-type="sourcefile">libs/ui/src/lib/i18n.ts</context>
@ -3610,7 +3610,7 @@
<target state="translated">Anualmente</target>
<context-group purpose="location">
<context context-type="sourcefile">apps/client/src/app/pages/portfolio/analysis/analysis-page.component.ts</context>
<context context-type="linenumber">104</context>
<context context-type="linenumber">99</context>
</context-group>
</trans-unit>
<trans-unit id="72640258012696878" datatype="html">
@ -4210,7 +4210,7 @@
</context-group>
<context-group purpose="location">
<context context-type="sourcefile">apps/client/src/app/components/admin-market-data/asset-profile-dialog/asset-profile-dialog.html</context>
<context context-type="linenumber">715</context>
<context context-type="linenumber">713</context>
</context-group>
<context-group purpose="location">
<context context-type="sourcefile">apps/client/src/app/components/admin-platform/admin-platform.component.html</context>
@ -4362,7 +4362,7 @@
<target state="new">Shares After</target>
<context-group purpose="location">
<context context-type="sourcefile">apps/client/src/app/components/admin-market-data/asset-profile-dialog/asset-profile-dialog.html</context>
<context context-type="linenumber">592</context>
<context context-type="linenumber">590</context>
</context-group>
</trans-unit>
<trans-unit id="5393065248443550644" datatype="html">
@ -4634,7 +4634,7 @@
<target state="translated">Configuração do raspador</target>
<context-group purpose="location">
<context context-type="sourcefile">apps/client/src/app/components/admin-market-data/asset-profile-dialog/asset-profile-dialog.html</context>
<context context-type="linenumber">636</context>
<context context-type="linenumber">634</context>
</context-group>
</trans-unit>
<trans-unit id="7504169991280318133" datatype="html">
@ -6156,7 +6156,7 @@
<target state="translated">Ops! Não foi possível analisar os dados históricos.</target>
<context-group purpose="location">
<context context-type="sourcefile">libs/ui/src/lib/historical-market-data-editor/historical-market-data-editor.component.ts</context>
<context context-type="linenumber">284</context>
<context context-type="linenumber">286</context>
</context-group>
</trans-unit>
<trans-unit id="7194017842579795231" datatype="html">
@ -6248,7 +6248,7 @@
<target state="translated">Teste</target>
<context-group purpose="location">
<context context-type="sourcefile">apps/client/src/app/components/admin-market-data/asset-profile-dialog/asset-profile-dialog.html</context>
<context context-type="linenumber">733</context>
<context context-type="linenumber">731</context>
</context-group>
</trans-unit>
<trans-unit id="6483680626836794824" datatype="html">
@ -6324,7 +6324,7 @@
<target state="translated">Investimento</target>
<context-group purpose="location">
<context context-type="sourcefile">apps/client/src/app/pages/portfolio/analysis/analysis-page.component.ts</context>
<context context-type="linenumber">109</context>
<context context-type="linenumber">104</context>
</context-group>
<context-group purpose="location">
<context context-type="sourcefile">apps/client/src/app/pages/resources/personal-finance-tools/product-page.component.ts</context>
@ -6516,7 +6516,7 @@
<target state="translated">Coleta de dados</target>
<context-group purpose="location">
<context context-type="sourcefile">apps/client/src/app/components/admin-market-data/asset-profile-dialog/asset-profile-dialog.html</context>
<context context-type="linenumber">756</context>
<context context-type="linenumber">754</context>
</context-group>
<context-group purpose="location">
<context context-type="sourcefile">apps/client/src/app/components/admin-overview/admin-overview.html</context>
@ -7081,7 +7081,7 @@
</context-group>
<context-group purpose="location">
<context context-type="sourcefile">apps/client/src/app/components/admin-market-data/asset-profile-dialog/asset-profile-dialog.html</context>
<context context-type="linenumber">761</context>
<context context-type="linenumber">759</context>
</context-group>
<context-group purpose="location">
<context context-type="sourcefile">apps/client/src/app/components/admin-market-data/create-asset-profile-dialog/create-asset-profile-dialog.html</context>
@ -7133,7 +7133,7 @@
<target state="translated">Fechar</target>
<context-group purpose="location">
<context context-type="sourcefile">apps/client/src/app/components/admin-market-data/asset-profile-dialog/asset-profile-dialog.html</context>
<context context-type="linenumber">763</context>
<context context-type="linenumber">761</context>
</context-group>
<context-group purpose="location">
<context context-type="sourcefile">apps/client/src/app/components/admin-market-data/create-asset-profile-dialog/create-asset-profile-dialog.html</context>
@ -7421,7 +7421,7 @@
<target state="new">Shares Before</target>
<context-group purpose="location">
<context context-type="sourcefile">apps/client/src/app/components/admin-market-data/asset-profile-dialog/asset-profile-dialog.html</context>
<context context-type="linenumber">605</context>
<context context-type="linenumber">603</context>
</context-group>
</trans-unit>
<trans-unit id="4605555143173907624" datatype="html">
@ -7711,7 +7711,7 @@
<target state="translated">Guardar</target>
<context-group purpose="location">
<context context-type="sourcefile">apps/client/src/app/components/admin-market-data/asset-profile-dialog/asset-profile-dialog.html</context>
<context context-type="linenumber">772</context>
<context context-type="linenumber">770</context>
</context-group>
<context-group purpose="location">
<context context-type="sourcefile">apps/client/src/app/components/admin-market-data/create-asset-profile-dialog/create-asset-profile-dialog.html</context>
@ -7763,7 +7763,7 @@
</context-group>
<context-group purpose="location">
<context context-type="sourcefile">apps/client/src/app/components/user-account-access/user-account-access.component.ts</context>
<context context-type="linenumber">255</context>
<context context-type="linenumber">257</context>
</context-group>
</trans-unit>
<trans-unit id="1769610706135259386" datatype="html">
@ -7811,7 +7811,7 @@
<target state="new">AI prompt has been copied to the clipboard</target>
<context-group purpose="location">
<context context-type="sourcefile">apps/client/src/app/pages/portfolio/analysis/analysis-page.component.ts</context>
<context context-type="linenumber">222</context>
<context context-type="linenumber">205</context>
</context-group>
</trans-unit>
<trans-unit id="1616747898909934803" datatype="html">
@ -7819,7 +7819,7 @@
<target state="translated">O link foi copiado para a área de transferência</target>
<context-group purpose="location">
<context context-type="sourcefile">apps/client/src/app/components/access-table/access-table.component.ts</context>
<context context-type="linenumber">107</context>
<context context-type="linenumber">112</context>
</context-group>
</trans-unit>
<trans-unit id="8540986733881734625" datatype="html">
@ -7843,7 +7843,7 @@
<target state="translated">Preço de mercado padrão</target>
<context-group purpose="location">
<context context-type="sourcefile">apps/client/src/app/components/admin-market-data/asset-profile-dialog/asset-profile-dialog.html</context>
<context context-type="linenumber">646</context>
<context context-type="linenumber">644</context>
</context-group>
</trans-unit>
<trans-unit id="1713271461473302108" datatype="html">
@ -7851,7 +7851,7 @@
<target state="new">Mode</target>
<context-group purpose="location">
<context context-type="sourcefile">apps/client/src/app/components/admin-market-data/asset-profile-dialog/asset-profile-dialog.html</context>
<context context-type="linenumber">683</context>
<context context-type="linenumber">681</context>
</context-group>
</trans-unit>
<trans-unit id="1717822775546188057" datatype="html">
@ -7867,7 +7867,7 @@
<target state="new">Selector</target>
<context-group purpose="location">
<context context-type="sourcefile">apps/client/src/app/components/admin-market-data/asset-profile-dialog/asset-profile-dialog.html</context>
<context context-type="linenumber">699</context>
<context context-type="linenumber">697</context>
</context-group>
</trans-unit>
<trans-unit id="645724892732039501" datatype="html">
@ -7875,7 +7875,7 @@
<target state="new">HTTP Request Headers</target>
<context-group purpose="location">
<context context-type="sourcefile">apps/client/src/app/components/admin-market-data/asset-profile-dialog/asset-profile-dialog.html</context>
<context context-type="linenumber">659</context>
<context context-type="linenumber">657</context>
</context-group>
</trans-unit>
<trans-unit id="8635324470284879211" datatype="html">
@ -7899,7 +7899,7 @@
<target state="new">Open Duck.ai</target>
<context-group purpose="location">
<context context-type="sourcefile">apps/client/src/app/pages/portfolio/analysis/analysis-page.component.ts</context>
<context context-type="linenumber">223</context>
<context context-type="linenumber">206</context>
</context-group>
</trans-unit>
<trans-unit id="5674286808255988565" datatype="html">

60
apps/client/src/locales/messages.tr.xlf

@ -275,7 +275,7 @@
<target state="translated">Bu erişim iznini geri almayı gerçekten istiyor musunuz?</target>
<context-group purpose="location">
<context context-type="sourcefile">apps/client/src/app/components/access-table/access-table.component.ts</context>
<context context-type="linenumber">121</context>
<context context-type="linenumber">126</context>
</context-group>
</trans-unit>
<trans-unit id="4029903570030679337" datatype="html">
@ -687,7 +687,7 @@
<target state="new">Splits</target>
<context-group purpose="location">
<context context-type="sourcefile">apps/client/src/app/components/admin-market-data/asset-profile-dialog/asset-profile-dialog.html</context>
<context context-type="linenumber">520</context>
<context context-type="linenumber">518</context>
</context-group>
</trans-unit>
<trans-unit id="267346373699222750" datatype="html">
@ -727,11 +727,11 @@
<target state="translated">Tarih</target>
<context-group purpose="location">
<context context-type="sourcefile">apps/client/src/app/components/admin-market-data/asset-profile-dialog/asset-profile-dialog.html</context>
<context context-type="linenumber">529</context>
<context context-type="linenumber">527</context>
</context-group>
<context-group purpose="location">
<context context-type="sourcefile">apps/client/src/app/components/admin-market-data/asset-profile-dialog/asset-profile-dialog.html</context>
<context context-type="linenumber">568</context>
<context context-type="linenumber">566</context>
</context-group>
<context-group purpose="location">
<context context-type="sourcefile">apps/client/src/app/pages/portfolio/activities/create-or-update-activity-dialog/create-or-update-activity-dialog.html</context>
@ -1047,7 +1047,7 @@
<target state="translated">Veri Toplayıcı Yapılandırması</target>
<context-group purpose="location">
<context context-type="sourcefile">apps/client/src/app/components/admin-market-data/asset-profile-dialog/asset-profile-dialog.html</context>
<context context-type="linenumber">636</context>
<context context-type="linenumber">634</context>
</context-group>
</trans-unit>
<trans-unit id="4388879716045736175" datatype="html">
@ -1219,7 +1219,7 @@
<target state="translated">Ekle</target>
<context-group purpose="location">
<context context-type="sourcefile">apps/client/src/app/components/admin-market-data/asset-profile-dialog/asset-profile-dialog.html</context>
<context context-type="linenumber">623</context>
<context context-type="linenumber">621</context>
</context-group>
<context-group purpose="location">
<context context-type="sourcefile">apps/client/src/app/components/admin-overview/admin-overview.html</context>
@ -1263,7 +1263,7 @@
</context-group>
<context-group purpose="location">
<context context-type="sourcefile">apps/client/src/app/components/admin-market-data/asset-profile-dialog/asset-profile-dialog.html</context>
<context context-type="linenumber">715</context>
<context context-type="linenumber">713</context>
</context-group>
<context-group purpose="location">
<context context-type="sourcefile">apps/client/src/app/components/admin-platform/admin-platform.component.html</context>
@ -1539,7 +1539,7 @@
<target state="new">Split Ratio</target>
<context-group purpose="location">
<context context-type="sourcefile">apps/client/src/app/components/admin-market-data/asset-profile-dialog/asset-profile-dialog.html</context>
<context context-type="linenumber">534</context>
<context context-type="linenumber">532</context>
</context-group>
</trans-unit>
<trans-unit id="5486880308148746399" datatype="html">
@ -3615,7 +3615,7 @@
<target state="new">Shares After</target>
<context-group purpose="location">
<context context-type="sourcefile">apps/client/src/app/components/admin-market-data/asset-profile-dialog/asset-profile-dialog.html</context>
<context context-type="linenumber">592</context>
<context context-type="linenumber">590</context>
</context-group>
</trans-unit>
<trans-unit id="8890553633144307762" datatype="html">
@ -3883,7 +3883,7 @@
</context-group>
<context-group purpose="location">
<context context-type="sourcefile">apps/client/src/app/pages/portfolio/analysis/analysis-page.component.ts</context>
<context context-type="linenumber">89</context>
<context context-type="linenumber">85</context>
</context-group>
<context-group purpose="location">
<context context-type="sourcefile">libs/ui/src/lib/i18n.ts</context>
@ -3903,7 +3903,7 @@
<target state="translated">Aylık</target>
<context-group purpose="location">
<context context-type="sourcefile">apps/client/src/app/pages/portfolio/analysis/analysis-page.component.ts</context>
<context context-type="linenumber">103</context>
<context context-type="linenumber">98</context>
</context-group>
</trans-unit>
<trans-unit id="8036977202721714375" datatype="html">
@ -3911,7 +3911,7 @@
<target state="translated">Yıllık</target>
<context-group purpose="location">
<context context-type="sourcefile">apps/client/src/app/pages/portfolio/analysis/analysis-page.component.ts</context>
<context context-type="linenumber">104</context>
<context context-type="linenumber">99</context>
</context-group>
</trans-unit>
<trans-unit id="6293970137138896363" datatype="html">
@ -4832,7 +4832,7 @@
<target state="translated">Yerel Ayarlar</target>
<context-group purpose="location">
<context context-type="sourcefile">apps/client/src/app/components/admin-market-data/asset-profile-dialog/asset-profile-dialog.html</context>
<context context-type="linenumber">674</context>
<context context-type="linenumber">672</context>
</context-group>
<context-group purpose="location">
<context context-type="sourcefile">apps/client/src/app/components/user-account-settings/user-account-settings.html</context>
@ -5764,7 +5764,7 @@
</context-group>
<context-group purpose="location">
<context context-type="sourcefile">apps/client/src/app/pages/portfolio/analysis/analysis-page.component.ts</context>
<context context-type="linenumber">93</context>
<context context-type="linenumber">88</context>
</context-group>
</trans-unit>
<trans-unit id="5957846001261659229" datatype="html">
@ -6156,7 +6156,7 @@
<target state="translated">Hay Allah! Geçmiş veriler ayrıştırılamadı.</target>
<context-group purpose="location">
<context context-type="sourcefile">libs/ui/src/lib/historical-market-data-editor/historical-market-data-editor.component.ts</context>
<context context-type="linenumber">284</context>
<context context-type="linenumber">286</context>
</context-group>
</trans-unit>
<trans-unit id="7194017842579795231" datatype="html">
@ -6248,7 +6248,7 @@
<target state="translated">Test</target>
<context-group purpose="location">
<context context-type="sourcefile">apps/client/src/app/components/admin-market-data/asset-profile-dialog/asset-profile-dialog.html</context>
<context context-type="linenumber">733</context>
<context context-type="linenumber">731</context>
</context-group>
</trans-unit>
<trans-unit id="6483680626836794824" datatype="html">
@ -6324,7 +6324,7 @@
<target state="translated">Yatırım</target>
<context-group purpose="location">
<context context-type="sourcefile">apps/client/src/app/pages/portfolio/analysis/analysis-page.component.ts</context>
<context context-type="linenumber">109</context>
<context context-type="linenumber">104</context>
</context-group>
<context-group purpose="location">
<context context-type="sourcefile">apps/client/src/app/pages/resources/personal-finance-tools/product-page.component.ts</context>
@ -6516,7 +6516,7 @@
<target state="translated">Veri Toplama</target>
<context-group purpose="location">
<context context-type="sourcefile">apps/client/src/app/components/admin-market-data/asset-profile-dialog/asset-profile-dialog.html</context>
<context context-type="linenumber">756</context>
<context context-type="linenumber">754</context>
</context-group>
<context-group purpose="location">
<context context-type="sourcefile">apps/client/src/app/components/admin-overview/admin-overview.html</context>
@ -7081,7 +7081,7 @@
</context-group>
<context-group purpose="location">
<context context-type="sourcefile">apps/client/src/app/components/admin-market-data/asset-profile-dialog/asset-profile-dialog.html</context>
<context context-type="linenumber">761</context>
<context context-type="linenumber">759</context>
</context-group>
<context-group purpose="location">
<context context-type="sourcefile">apps/client/src/app/components/admin-market-data/create-asset-profile-dialog/create-asset-profile-dialog.html</context>
@ -7133,7 +7133,7 @@
<target state="translated">Kapat</target>
<context-group purpose="location">
<context context-type="sourcefile">apps/client/src/app/components/admin-market-data/asset-profile-dialog/asset-profile-dialog.html</context>
<context context-type="linenumber">763</context>
<context context-type="linenumber">761</context>
</context-group>
<context-group purpose="location">
<context context-type="sourcefile">apps/client/src/app/components/admin-market-data/create-asset-profile-dialog/create-asset-profile-dialog.html</context>
@ -7421,7 +7421,7 @@
<target state="new">Shares Before</target>
<context-group purpose="location">
<context context-type="sourcefile">apps/client/src/app/components/admin-market-data/asset-profile-dialog/asset-profile-dialog.html</context>
<context context-type="linenumber">605</context>
<context context-type="linenumber">603</context>
</context-group>
</trans-unit>
<trans-unit id="4605555143173907624" datatype="html">
@ -7711,7 +7711,7 @@
<target state="translated">Kaydet</target>
<context-group purpose="location">
<context context-type="sourcefile">apps/client/src/app/components/admin-market-data/asset-profile-dialog/asset-profile-dialog.html</context>
<context context-type="linenumber">772</context>
<context context-type="linenumber">770</context>
</context-group>
<context-group purpose="location">
<context context-type="sourcefile">apps/client/src/app/components/admin-market-data/create-asset-profile-dialog/create-asset-profile-dialog.html</context>
@ -7763,7 +7763,7 @@
</context-group>
<context-group purpose="location">
<context context-type="sourcefile">apps/client/src/app/components/user-account-access/user-account-access.component.ts</context>
<context context-type="linenumber">255</context>
<context context-type="linenumber">257</context>
</context-group>
</trans-unit>
<trans-unit id="1769610706135259386" datatype="html">
@ -7811,7 +7811,7 @@
<target state="translated">Yapay zeka istemi panoya kopyalandı</target>
<context-group purpose="location">
<context context-type="sourcefile">apps/client/src/app/pages/portfolio/analysis/analysis-page.component.ts</context>
<context context-type="linenumber">222</context>
<context context-type="linenumber">205</context>
</context-group>
</trans-unit>
<trans-unit id="1616747898909934803" datatype="html">
@ -7819,7 +7819,7 @@
<target state="translated">Bağlantı panoya kopyalandı</target>
<context-group purpose="location">
<context context-type="sourcefile">apps/client/src/app/components/access-table/access-table.component.ts</context>
<context context-type="linenumber">107</context>
<context context-type="linenumber">112</context>
</context-group>
</trans-unit>
<trans-unit id="8540986733881734625" datatype="html">
@ -7843,7 +7843,7 @@
<target state="translated">Varsayılan Piyasa Fiyatı</target>
<context-group purpose="location">
<context context-type="sourcefile">apps/client/src/app/components/admin-market-data/asset-profile-dialog/asset-profile-dialog.html</context>
<context context-type="linenumber">646</context>
<context context-type="linenumber">644</context>
</context-group>
</trans-unit>
<trans-unit id="1713271461473302108" datatype="html">
@ -7851,7 +7851,7 @@
<target state="translated">Mod</target>
<context-group purpose="location">
<context context-type="sourcefile">apps/client/src/app/components/admin-market-data/asset-profile-dialog/asset-profile-dialog.html</context>
<context context-type="linenumber">683</context>
<context context-type="linenumber">681</context>
</context-group>
</trans-unit>
<trans-unit id="1717822775546188057" datatype="html">
@ -7867,7 +7867,7 @@
<target state="translated">Seçici</target>
<context-group purpose="location">
<context context-type="sourcefile">apps/client/src/app/components/admin-market-data/asset-profile-dialog/asset-profile-dialog.html</context>
<context context-type="linenumber">699</context>
<context context-type="linenumber">697</context>
</context-group>
</trans-unit>
<trans-unit id="645724892732039501" datatype="html">
@ -7875,7 +7875,7 @@
<target state="translated">HTTP İstek Başlıkları</target>
<context-group purpose="location">
<context context-type="sourcefile">apps/client/src/app/components/admin-market-data/asset-profile-dialog/asset-profile-dialog.html</context>
<context context-type="linenumber">659</context>
<context context-type="linenumber">657</context>
</context-group>
</trans-unit>
<trans-unit id="8635324470284879211" datatype="html">
@ -7899,7 +7899,7 @@
<target state="translated">Duck.ai’yi aç</target>
<context-group purpose="location">
<context context-type="sourcefile">apps/client/src/app/pages/portfolio/analysis/analysis-page.component.ts</context>
<context context-type="linenumber">223</context>
<context context-type="linenumber">206</context>
</context-group>
</trans-unit>
<trans-unit id="5674286808255988565" datatype="html">

60
apps/client/src/locales/messages.uk.xlf

@ -391,7 +391,7 @@
<target state="translated">Посилання скопійовано в буфер обміну</target>
<context-group purpose="location">
<context context-type="sourcefile">apps/client/src/app/components/access-table/access-table.component.ts</context>
<context context-type="linenumber">107</context>
<context context-type="linenumber">112</context>
</context-group>
</trans-unit>
<trans-unit id="8264698726451826067" datatype="html">
@ -399,7 +399,7 @@
<target state="translated">Ви дійсно хочете відкликати цей наданий доступ?</target>
<context-group purpose="location">
<context context-type="sourcefile">apps/client/src/app/components/access-table/access-table.component.ts</context>
<context context-type="linenumber">121</context>
<context context-type="linenumber">126</context>
</context-group>
</trans-unit>
<trans-unit id="4029903570030679337" datatype="html">
@ -871,7 +871,7 @@
<target state="new">Splits</target>
<context-group purpose="location">
<context context-type="sourcefile">apps/client/src/app/components/admin-market-data/asset-profile-dialog/asset-profile-dialog.html</context>
<context context-type="linenumber">520</context>
<context context-type="linenumber">518</context>
</context-group>
</trans-unit>
<trans-unit id="267346373699222750" datatype="html">
@ -1239,7 +1239,7 @@
<target state="translated">Конфігурація скребка</target>
<context-group purpose="location">
<context context-type="sourcefile">apps/client/src/app/components/admin-market-data/asset-profile-dialog/asset-profile-dialog.html</context>
<context context-type="linenumber">636</context>
<context context-type="linenumber">634</context>
</context-group>
</trans-unit>
<trans-unit id="6563391987554512024" datatype="html">
@ -1247,7 +1247,7 @@
<target state="translated">Тест</target>
<context-group purpose="location">
<context context-type="sourcefile">apps/client/src/app/components/admin-market-data/asset-profile-dialog/asset-profile-dialog.html</context>
<context context-type="linenumber">733</context>
<context context-type="linenumber">731</context>
</context-group>
</trans-unit>
<trans-unit id="8308045076391224954" datatype="html">
@ -1259,7 +1259,7 @@
</context-group>
<context-group purpose="location">
<context context-type="sourcefile">apps/client/src/app/components/admin-market-data/asset-profile-dialog/asset-profile-dialog.html</context>
<context context-type="linenumber">715</context>
<context context-type="linenumber">713</context>
</context-group>
<context-group purpose="location">
<context context-type="sourcefile">apps/client/src/app/components/admin-platform/admin-platform.component.html</context>
@ -1463,7 +1463,7 @@
<target state="translated">Збір даних</target>
<context-group purpose="location">
<context context-type="sourcefile">apps/client/src/app/components/admin-market-data/asset-profile-dialog/asset-profile-dialog.html</context>
<context context-type="linenumber">756</context>
<context context-type="linenumber">754</context>
</context-group>
<context-group purpose="location">
<context context-type="sourcefile">apps/client/src/app/components/admin-overview/admin-overview.html</context>
@ -1507,7 +1507,7 @@
<target state="translated">Додати</target>
<context-group purpose="location">
<context context-type="sourcefile">apps/client/src/app/components/admin-market-data/asset-profile-dialog/asset-profile-dialog.html</context>
<context context-type="linenumber">623</context>
<context context-type="linenumber">621</context>
</context-group>
<context-group purpose="location">
<context context-type="sourcefile">apps/client/src/app/components/admin-overview/admin-overview.html</context>
@ -2127,7 +2127,7 @@
<target state="new">Split Ratio</target>
<context-group purpose="location">
<context context-type="sourcefile">apps/client/src/app/components/admin-market-data/asset-profile-dialog/asset-profile-dialog.html</context>
<context context-type="linenumber">534</context>
<context context-type="linenumber">532</context>
</context-group>
</trans-unit>
<trans-unit id="5486880308148746399" datatype="html">
@ -2459,7 +2459,7 @@
<target state="translated">Зберегти</target>
<context-group purpose="location">
<context context-type="sourcefile">apps/client/src/app/components/admin-market-data/asset-profile-dialog/asset-profile-dialog.html</context>
<context context-type="linenumber">772</context>
<context context-type="linenumber">770</context>
</context-group>
<context-group purpose="location">
<context context-type="sourcefile">apps/client/src/app/components/admin-market-data/create-asset-profile-dialog/create-asset-profile-dialog.html</context>
@ -2875,7 +2875,7 @@
</context-group>
<context-group purpose="location">
<context context-type="sourcefile">apps/client/src/app/components/user-account-access/user-account-access.component.ts</context>
<context context-type="linenumber">255</context>
<context context-type="linenumber">257</context>
</context-group>
</trans-unit>
<trans-unit id="1769610706135259386" datatype="html">
@ -3143,7 +3143,7 @@
<target state="translated">Локалізація</target>
<context-group purpose="location">
<context context-type="sourcefile">apps/client/src/app/components/admin-market-data/asset-profile-dialog/asset-profile-dialog.html</context>
<context context-type="linenumber">674</context>
<context context-type="linenumber">672</context>
</context-group>
<context-group purpose="location">
<context context-type="sourcefile">apps/client/src/app/components/user-account-settings/user-account-settings.html</context>
@ -4772,11 +4772,11 @@
<target state="translated">Дата</target>
<context-group purpose="location">
<context context-type="sourcefile">apps/client/src/app/components/admin-market-data/asset-profile-dialog/asset-profile-dialog.html</context>
<context context-type="linenumber">529</context>
<context context-type="linenumber">527</context>
</context-group>
<context-group purpose="location">
<context context-type="sourcefile">apps/client/src/app/components/admin-market-data/asset-profile-dialog/asset-profile-dialog.html</context>
<context context-type="linenumber">568</context>
<context context-type="linenumber">566</context>
</context-group>
<context-group purpose="location">
<context context-type="sourcefile">apps/client/src/app/pages/portfolio/activities/create-or-update-activity-dialog/create-or-update-activity-dialog.html</context>
@ -4944,7 +4944,7 @@
<target state="new">Shares After</target>
<context-group purpose="location">
<context context-type="sourcefile">apps/client/src/app/components/admin-market-data/asset-profile-dialog/asset-profile-dialog.html</context>
<context context-type="linenumber">592</context>
<context context-type="linenumber">590</context>
</context-group>
</trans-unit>
<trans-unit id="8890553633144307762" datatype="html">
@ -5228,7 +5228,7 @@
</context-group>
<context-group purpose="location">
<context context-type="sourcefile">apps/client/src/app/pages/portfolio/analysis/analysis-page.component.ts</context>
<context context-type="linenumber">89</context>
<context context-type="linenumber">85</context>
</context-group>
<context-group purpose="location">
<context context-type="sourcefile">libs/ui/src/lib/i18n.ts</context>
@ -5240,7 +5240,7 @@
<target state="translated">Інвестиції</target>
<context-group purpose="location">
<context context-type="sourcefile">apps/client/src/app/pages/portfolio/analysis/analysis-page.component.ts</context>
<context context-type="linenumber">109</context>
<context context-type="linenumber">104</context>
</context-group>
<context-group purpose="location">
<context context-type="sourcefile">apps/client/src/app/pages/resources/personal-finance-tools/product-page.component.ts</context>
@ -5260,7 +5260,7 @@
<target state="translated">Щомісячно</target>
<context-group purpose="location">
<context context-type="sourcefile">apps/client/src/app/pages/portfolio/analysis/analysis-page.component.ts</context>
<context context-type="linenumber">103</context>
<context context-type="linenumber">98</context>
</context-group>
</trans-unit>
<trans-unit id="8036977202721714375" datatype="html">
@ -5268,7 +5268,7 @@
<target state="translated">Щорічно</target>
<context-group purpose="location">
<context context-type="sourcefile">apps/client/src/app/pages/portfolio/analysis/analysis-page.component.ts</context>
<context context-type="linenumber">104</context>
<context context-type="linenumber">99</context>
</context-group>
</trans-unit>
<trans-unit id="1600023202562292052" datatype="html">
@ -6343,7 +6343,7 @@
<target state="new">Shares Before</target>
<context-group purpose="location">
<context context-type="sourcefile">apps/client/src/app/components/admin-market-data/asset-profile-dialog/asset-profile-dialog.html</context>
<context context-type="linenumber">605</context>
<context context-type="linenumber">603</context>
</context-group>
</trans-unit>
<trans-unit id="4605555143173907624" datatype="html">
@ -6923,7 +6923,7 @@
<target state="translated">Упс! Не вдалося отримати історичні дані.</target>
<context-group purpose="location">
<context context-type="sourcefile">libs/ui/src/lib/historical-market-data-editor/historical-market-data-editor.component.ts</context>
<context context-type="linenumber">284</context>
<context context-type="linenumber">286</context>
</context-group>
</trans-unit>
<trans-unit id="7194017842579795231" datatype="html">
@ -7079,7 +7079,7 @@
</context-group>
<context-group purpose="location">
<context context-type="sourcefile">apps/client/src/app/components/admin-market-data/asset-profile-dialog/asset-profile-dialog.html</context>
<context context-type="linenumber">761</context>
<context context-type="linenumber">759</context>
</context-group>
<context-group purpose="location">
<context context-type="sourcefile">apps/client/src/app/components/admin-market-data/create-asset-profile-dialog/create-asset-profile-dialog.html</context>
@ -7139,7 +7139,7 @@
<target state="translated">Закрити</target>
<context-group purpose="location">
<context context-type="sourcefile">apps/client/src/app/components/admin-market-data/asset-profile-dialog/asset-profile-dialog.html</context>
<context context-type="linenumber">763</context>
<context context-type="linenumber">761</context>
</context-group>
<context-group purpose="location">
<context context-type="sourcefile">apps/client/src/app/components/admin-market-data/create-asset-profile-dialog/create-asset-profile-dialog.html</context>
@ -7695,7 +7695,7 @@
</context-group>
<context-group purpose="location">
<context context-type="sourcefile">apps/client/src/app/pages/portfolio/analysis/analysis-page.component.ts</context>
<context context-type="linenumber">93</context>
<context context-type="linenumber">88</context>
</context-group>
</trans-unit>
<trans-unit id="5957846001261659229" datatype="html">
@ -7819,7 +7819,7 @@
<target state="translated">Запит AI скопійовано в буфер обміну</target>
<context-group purpose="location">
<context context-type="sourcefile">apps/client/src/app/pages/portfolio/analysis/analysis-page.component.ts</context>
<context context-type="linenumber">222</context>
<context context-type="linenumber">205</context>
</context-group>
</trans-unit>
<trans-unit id="8540986733881734625" datatype="html">
@ -7843,7 +7843,7 @@
<target state="translated">Стандартна ринкова ціна</target>
<context-group purpose="location">
<context context-type="sourcefile">apps/client/src/app/components/admin-market-data/asset-profile-dialog/asset-profile-dialog.html</context>
<context context-type="linenumber">646</context>
<context context-type="linenumber">644</context>
</context-group>
</trans-unit>
<trans-unit id="1713271461473302108" datatype="html">
@ -7851,7 +7851,7 @@
<target state="translated">Режим</target>
<context-group purpose="location">
<context context-type="sourcefile">apps/client/src/app/components/admin-market-data/asset-profile-dialog/asset-profile-dialog.html</context>
<context context-type="linenumber">683</context>
<context context-type="linenumber">681</context>
</context-group>
</trans-unit>
<trans-unit id="1717822775546188057" datatype="html">
@ -7867,7 +7867,7 @@
<target state="translated">Селектор</target>
<context-group purpose="location">
<context context-type="sourcefile">apps/client/src/app/components/admin-market-data/asset-profile-dialog/asset-profile-dialog.html</context>
<context context-type="linenumber">699</context>
<context context-type="linenumber">697</context>
</context-group>
</trans-unit>
<trans-unit id="645724892732039501" datatype="html">
@ -7875,7 +7875,7 @@
<target state="translated">Заголовки HTTP-запиту</target>
<context-group purpose="location">
<context context-type="sourcefile">apps/client/src/app/components/admin-market-data/asset-profile-dialog/asset-profile-dialog.html</context>
<context context-type="linenumber">659</context>
<context context-type="linenumber">657</context>
</context-group>
</trans-unit>
<trans-unit id="8635324470284879211" datatype="html">
@ -7899,7 +7899,7 @@
<target state="translated">Відкрити Duck.ai</target>
<context-group purpose="location">
<context context-type="sourcefile">apps/client/src/app/pages/portfolio/analysis/analysis-page.component.ts</context>
<context context-type="linenumber">223</context>
<context context-type="linenumber">206</context>
</context-group>
</trans-unit>
<trans-unit id="5674286808255988565" datatype="html">

60
apps/client/src/locales/messages.xlf

@ -283,7 +283,7 @@
<source>Do you really want to revoke this granted access?</source>
<context-group purpose="location">
<context context-type="sourcefile">apps/client/src/app/components/access-table/access-table.component.ts</context>
<context context-type="linenumber">121</context>
<context context-type="linenumber">126</context>
</context-group>
</trans-unit>
<trans-unit id="4029903570030679337" datatype="html">
@ -686,7 +686,7 @@
<source>Splits</source>
<context-group purpose="location">
<context context-type="sourcefile">apps/client/src/app/components/admin-market-data/asset-profile-dialog/asset-profile-dialog.html</context>
<context context-type="linenumber">520</context>
<context context-type="linenumber">518</context>
</context-group>
</trans-unit>
<trans-unit id="267346373699222750" datatype="html">
@ -721,11 +721,11 @@
<source>Date</source>
<context-group purpose="location">
<context context-type="sourcefile">apps/client/src/app/components/admin-market-data/asset-profile-dialog/asset-profile-dialog.html</context>
<context context-type="linenumber">529</context>
<context context-type="linenumber">527</context>
</context-group>
<context-group purpose="location">
<context context-type="sourcefile">apps/client/src/app/components/admin-market-data/asset-profile-dialog/asset-profile-dialog.html</context>
<context context-type="linenumber">568</context>
<context context-type="linenumber">566</context>
</context-group>
<context-group purpose="location">
<context context-type="sourcefile">apps/client/src/app/pages/portfolio/activities/create-or-update-activity-dialog/create-or-update-activity-dialog.html</context>
@ -924,7 +924,7 @@
<source>Oops! Could not parse historical data.</source>
<context-group purpose="location">
<context context-type="sourcefile">libs/ui/src/lib/historical-market-data-editor/historical-market-data-editor.component.ts</context>
<context context-type="linenumber">284</context>
<context context-type="linenumber">286</context>
</context-group>
</trans-unit>
<trans-unit id="7194017842579795231" datatype="html">
@ -1073,7 +1073,7 @@
<source>Scraper Configuration</source>
<context-group purpose="location">
<context context-type="sourcefile">apps/client/src/app/components/admin-market-data/asset-profile-dialog/asset-profile-dialog.html</context>
<context context-type="linenumber">636</context>
<context context-type="linenumber">634</context>
</context-group>
</trans-unit>
<trans-unit id="4388879716045736175" datatype="html">
@ -1248,7 +1248,7 @@
<source>Add</source>
<context-group purpose="location">
<context context-type="sourcefile">apps/client/src/app/components/admin-market-data/asset-profile-dialog/asset-profile-dialog.html</context>
<context context-type="linenumber">623</context>
<context context-type="linenumber">621</context>
</context-group>
<context-group purpose="location">
<context context-type="sourcefile">apps/client/src/app/components/admin-overview/admin-overview.html</context>
@ -1288,7 +1288,7 @@
</context-group>
<context-group purpose="location">
<context context-type="sourcefile">apps/client/src/app/components/admin-market-data/asset-profile-dialog/asset-profile-dialog.html</context>
<context context-type="linenumber">715</context>
<context context-type="linenumber">713</context>
</context-group>
<context-group purpose="location">
<context context-type="sourcefile">apps/client/src/app/components/admin-platform/admin-platform.component.html</context>
@ -1581,7 +1581,7 @@
<source>Split Ratio</source>
<context-group purpose="location">
<context context-type="sourcefile">apps/client/src/app/components/admin-market-data/asset-profile-dialog/asset-profile-dialog.html</context>
<context context-type="linenumber">534</context>
<context context-type="linenumber">532</context>
</context-group>
</trans-unit>
<trans-unit id="5486880308148746399" datatype="html">
@ -2335,7 +2335,7 @@
<source>Locale</source>
<context-group purpose="location">
<context context-type="sourcefile">apps/client/src/app/components/admin-market-data/asset-profile-dialog/asset-profile-dialog.html</context>
<context context-type="linenumber">674</context>
<context context-type="linenumber">672</context>
</context-group>
<context-group purpose="location">
<context context-type="sourcefile">apps/client/src/app/components/user-account-settings/user-account-settings.html</context>
@ -3889,7 +3889,7 @@
<source>Shares After</source>
<context-group purpose="location">
<context context-type="sourcefile">apps/client/src/app/components/admin-market-data/asset-profile-dialog/asset-profile-dialog.html</context>
<context context-type="linenumber">592</context>
<context context-type="linenumber">590</context>
</context-group>
</trans-unit>
<trans-unit id="8890553633144307762" datatype="html">
@ -4116,7 +4116,7 @@
</context-group>
<context-group purpose="location">
<context context-type="sourcefile">apps/client/src/app/pages/portfolio/analysis/analysis-page.component.ts</context>
<context context-type="linenumber">89</context>
<context context-type="linenumber">85</context>
</context-group>
<context-group purpose="location">
<context context-type="sourcefile">libs/ui/src/lib/i18n.ts</context>
@ -4134,14 +4134,14 @@
<source>Monthly</source>
<context-group purpose="location">
<context context-type="sourcefile">apps/client/src/app/pages/portfolio/analysis/analysis-page.component.ts</context>
<context context-type="linenumber">103</context>
<context context-type="linenumber">98</context>
</context-group>
</trans-unit>
<trans-unit id="8036977202721714375" datatype="html">
<source>Yearly</source>
<context-group purpose="location">
<context context-type="sourcefile">apps/client/src/app/pages/portfolio/analysis/analysis-page.component.ts</context>
<context context-type="linenumber">104</context>
<context context-type="linenumber">99</context>
</context-group>
</trans-unit>
<trans-unit id="6293970137138896363" datatype="html">
@ -5609,7 +5609,7 @@
</context-group>
<context-group purpose="location">
<context context-type="sourcefile">apps/client/src/app/pages/portfolio/analysis/analysis-page.component.ts</context>
<context context-type="linenumber">93</context>
<context context-type="linenumber">88</context>
</context-group>
</trans-unit>
<trans-unit id="5957846001261659229" datatype="html">
@ -5710,7 +5710,7 @@
<source>Test</source>
<context-group purpose="location">
<context context-type="sourcefile">apps/client/src/app/components/admin-market-data/asset-profile-dialog/asset-profile-dialog.html</context>
<context context-type="linenumber">733</context>
<context context-type="linenumber">731</context>
</context-group>
</trans-unit>
<trans-unit id="2570446216260149991" datatype="html">
@ -5792,7 +5792,7 @@
<source>Investment</source>
<context-group purpose="location">
<context context-type="sourcefile">apps/client/src/app/pages/portfolio/analysis/analysis-page.component.ts</context>
<context context-type="linenumber">109</context>
<context context-type="linenumber">104</context>
</context-group>
<context-group purpose="location">
<context context-type="sourcefile">apps/client/src/app/pages/resources/personal-finance-tools/product-page.component.ts</context>
@ -5966,7 +5966,7 @@
<source>Data Gathering</source>
<context-group purpose="location">
<context context-type="sourcefile">apps/client/src/app/components/admin-market-data/asset-profile-dialog/asset-profile-dialog.html</context>
<context context-type="linenumber">756</context>
<context context-type="linenumber">754</context>
</context-group>
<context-group purpose="location">
<context context-type="sourcefile">apps/client/src/app/components/admin-overview/admin-overview.html</context>
@ -6420,7 +6420,7 @@
</context-group>
<context-group purpose="location">
<context context-type="sourcefile">apps/client/src/app/components/admin-market-data/asset-profile-dialog/asset-profile-dialog.html</context>
<context context-type="linenumber">761</context>
<context context-type="linenumber">759</context>
</context-group>
<context-group purpose="location">
<context context-type="sourcefile">apps/client/src/app/components/admin-market-data/create-asset-profile-dialog/create-asset-profile-dialog.html</context>
@ -6492,7 +6492,7 @@
<source>Close</source>
<context-group purpose="location">
<context context-type="sourcefile">apps/client/src/app/components/admin-market-data/asset-profile-dialog/asset-profile-dialog.html</context>
<context context-type="linenumber">763</context>
<context context-type="linenumber">761</context>
</context-group>
<context-group purpose="location">
<context context-type="sourcefile">apps/client/src/app/components/admin-market-data/create-asset-profile-dialog/create-asset-profile-dialog.html</context>
@ -6760,7 +6760,7 @@
<source>Shares Before</source>
<context-group purpose="location">
<context context-type="sourcefile">apps/client/src/app/components/admin-market-data/asset-profile-dialog/asset-profile-dialog.html</context>
<context context-type="linenumber">605</context>
<context context-type="linenumber">603</context>
</context-group>
</trans-unit>
<trans-unit id="4605555143173907624" datatype="html">
@ -7019,7 +7019,7 @@
<source>Save</source>
<context-group purpose="location">
<context context-type="sourcefile">apps/client/src/app/components/admin-market-data/asset-profile-dialog/asset-profile-dialog.html</context>
<context context-type="linenumber">772</context>
<context context-type="linenumber">770</context>
</context-group>
<context-group purpose="location">
<context context-type="sourcefile">apps/client/src/app/components/admin-market-data/create-asset-profile-dialog/create-asset-profile-dialog.html</context>
@ -7084,7 +7084,7 @@
</context-group>
<context-group purpose="location">
<context context-type="sourcefile">apps/client/src/app/components/user-account-access/user-account-access.component.ts</context>
<context context-type="linenumber">255</context>
<context context-type="linenumber">257</context>
</context-group>
</trans-unit>
<trans-unit id="4060547242431613838" datatype="html">
@ -7112,21 +7112,21 @@
<source>AI prompt has been copied to the clipboard</source>
<context-group purpose="location">
<context context-type="sourcefile">apps/client/src/app/pages/portfolio/analysis/analysis-page.component.ts</context>
<context context-type="linenumber">222</context>
<context context-type="linenumber">205</context>
</context-group>
</trans-unit>
<trans-unit id="1616747898909934803" datatype="html">
<source>Link has been copied to the clipboard</source>
<context-group purpose="location">
<context context-type="sourcefile">apps/client/src/app/components/access-table/access-table.component.ts</context>
<context context-type="linenumber">107</context>
<context context-type="linenumber">112</context>
</context-group>
</trans-unit>
<trans-unit id="1713271461473302108" datatype="html">
<source>Mode</source>
<context-group purpose="location">
<context context-type="sourcefile">apps/client/src/app/components/admin-market-data/asset-profile-dialog/asset-profile-dialog.html</context>
<context context-type="linenumber">683</context>
<context context-type="linenumber">681</context>
</context-group>
</trans-unit>
<trans-unit id="1717822775546188057" datatype="html">
@ -7140,14 +7140,14 @@
<source>Default Market Price</source>
<context-group purpose="location">
<context context-type="sourcefile">apps/client/src/app/components/admin-market-data/asset-profile-dialog/asset-profile-dialog.html</context>
<context context-type="linenumber">646</context>
<context context-type="linenumber">644</context>
</context-group>
</trans-unit>
<trans-unit id="3540108566782816830" datatype="html">
<source>Selector</source>
<context-group purpose="location">
<context context-type="sourcefile">apps/client/src/app/components/admin-market-data/asset-profile-dialog/asset-profile-dialog.html</context>
<context context-type="linenumber">699</context>
<context context-type="linenumber">697</context>
</context-group>
</trans-unit>
<trans-unit id="6882618704933649036" datatype="html">
@ -7168,7 +7168,7 @@
<source>HTTP Request Headers</source>
<context-group purpose="location">
<context context-type="sourcefile">apps/client/src/app/components/admin-market-data/asset-profile-dialog/asset-profile-dialog.html</context>
<context context-type="linenumber">659</context>
<context context-type="linenumber">657</context>
</context-group>
</trans-unit>
<trans-unit id="4547068148181074902" datatype="html">
@ -7189,7 +7189,7 @@
<source>Open Duck.ai</source>
<context-group purpose="location">
<context context-type="sourcefile">apps/client/src/app/pages/portfolio/analysis/analysis-page.component.ts</context>
<context context-type="linenumber">223</context>
<context context-type="linenumber">206</context>
</context-group>
</trans-unit>
<trans-unit id="5674286808255988565" datatype="html">

60
apps/client/src/locales/messages.zh.xlf

@ -304,7 +304,7 @@
<target state="translated">您真的要撤销此访问权限吗?</target>
<context-group purpose="location">
<context context-type="sourcefile">apps/client/src/app/components/access-table/access-table.component.ts</context>
<context context-type="linenumber">121</context>
<context context-type="linenumber">126</context>
</context-group>
</trans-unit>
<trans-unit id="4029903570030679337" datatype="html">
@ -732,7 +732,7 @@
<target state="new">Splits</target>
<context-group purpose="location">
<context context-type="sourcefile">apps/client/src/app/components/admin-market-data/asset-profile-dialog/asset-profile-dialog.html</context>
<context context-type="linenumber">520</context>
<context context-type="linenumber">518</context>
</context-group>
</trans-unit>
<trans-unit id="267346373699222750" datatype="html">
@ -772,11 +772,11 @@
<target state="translated">日期</target>
<context-group purpose="location">
<context context-type="sourcefile">apps/client/src/app/components/admin-market-data/asset-profile-dialog/asset-profile-dialog.html</context>
<context context-type="linenumber">529</context>
<context context-type="linenumber">527</context>
</context-group>
<context-group purpose="location">
<context context-type="sourcefile">apps/client/src/app/components/admin-market-data/asset-profile-dialog/asset-profile-dialog.html</context>
<context context-type="linenumber">568</context>
<context context-type="linenumber">566</context>
</context-group>
<context-group purpose="location">
<context context-type="sourcefile">apps/client/src/app/pages/portfolio/activities/create-or-update-activity-dialog/create-or-update-activity-dialog.html</context>
@ -980,7 +980,7 @@
<target state="translated">哎呀!无法解析历史数据。</target>
<context-group purpose="location">
<context context-type="sourcefile">libs/ui/src/lib/historical-market-data-editor/historical-market-data-editor.component.ts</context>
<context context-type="linenumber">284</context>
<context context-type="linenumber">286</context>
</context-group>
</trans-unit>
<trans-unit id="7194017842579795231" datatype="html">
@ -1136,7 +1136,7 @@
<target state="translated">刮削配置</target>
<context-group purpose="location">
<context context-type="sourcefile">apps/client/src/app/components/admin-market-data/asset-profile-dialog/asset-profile-dialog.html</context>
<context context-type="linenumber">636</context>
<context context-type="linenumber">634</context>
</context-group>
</trans-unit>
<trans-unit id="4388879716045736175" datatype="html">
@ -1332,7 +1332,7 @@
<target state="translated">添加</target>
<context-group purpose="location">
<context context-type="sourcefile">apps/client/src/app/components/admin-market-data/asset-profile-dialog/asset-profile-dialog.html</context>
<context context-type="linenumber">623</context>
<context context-type="linenumber">621</context>
</context-group>
<context-group purpose="location">
<context context-type="sourcefile">apps/client/src/app/components/admin-overview/admin-overview.html</context>
@ -1376,7 +1376,7 @@
</context-group>
<context-group purpose="location">
<context context-type="sourcefile">apps/client/src/app/components/admin-market-data/asset-profile-dialog/asset-profile-dialog.html</context>
<context context-type="linenumber">715</context>
<context context-type="linenumber">713</context>
</context-group>
<context-group purpose="location">
<context context-type="sourcefile">apps/client/src/app/components/admin-platform/admin-platform.component.html</context>
@ -1700,7 +1700,7 @@
<target state="new">Split Ratio</target>
<context-group purpose="location">
<context context-type="sourcefile">apps/client/src/app/components/admin-market-data/asset-profile-dialog/asset-profile-dialog.html</context>
<context context-type="linenumber">534</context>
<context context-type="linenumber">532</context>
</context-group>
</trans-unit>
<trans-unit id="5486880308148746399" datatype="html">
@ -2532,7 +2532,7 @@
<target state="translated">语言环境</target>
<context-group purpose="location">
<context context-type="sourcefile">apps/client/src/app/components/admin-market-data/asset-profile-dialog/asset-profile-dialog.html</context>
<context context-type="linenumber">674</context>
<context context-type="linenumber">672</context>
</context-group>
<context-group purpose="location">
<context context-type="sourcefile">apps/client/src/app/components/user-account-settings/user-account-settings.html</context>
@ -4240,7 +4240,7 @@
<target state="new">Shares After</target>
<context-group purpose="location">
<context context-type="sourcefile">apps/client/src/app/components/admin-market-data/asset-profile-dialog/asset-profile-dialog.html</context>
<context context-type="linenumber">592</context>
<context context-type="linenumber">590</context>
</context-group>
</trans-unit>
<trans-unit id="8890553633144307762" datatype="html">
@ -4492,7 +4492,7 @@
</context-group>
<context-group purpose="location">
<context context-type="sourcefile">apps/client/src/app/pages/portfolio/analysis/analysis-page.component.ts</context>
<context context-type="linenumber">89</context>
<context context-type="linenumber">85</context>
</context-group>
<context-group purpose="location">
<context context-type="sourcefile">libs/ui/src/lib/i18n.ts</context>
@ -4512,7 +4512,7 @@
<target state="translated">每月</target>
<context-group purpose="location">
<context context-type="sourcefile">apps/client/src/app/pages/portfolio/analysis/analysis-page.component.ts</context>
<context context-type="linenumber">103</context>
<context context-type="linenumber">98</context>
</context-group>
</trans-unit>
<trans-unit id="8036977202721714375" datatype="html">
@ -4520,7 +4520,7 @@
<target state="translated">每年</target>
<context-group purpose="location">
<context context-type="sourcefile">apps/client/src/app/pages/portfolio/analysis/analysis-page.component.ts</context>
<context context-type="linenumber">104</context>
<context context-type="linenumber">99</context>
</context-group>
</trans-unit>
<trans-unit id="6293970137138896363" datatype="html">
@ -6145,7 +6145,7 @@
</context-group>
<context-group purpose="location">
<context context-type="sourcefile">apps/client/src/app/pages/portfolio/analysis/analysis-page.component.ts</context>
<context context-type="linenumber">93</context>
<context context-type="linenumber">88</context>
</context-group>
</trans-unit>
<trans-unit id="5957846001261659229" datatype="html">
@ -6257,7 +6257,7 @@
<target state="translated">测试</target>
<context-group purpose="location">
<context context-type="sourcefile">apps/client/src/app/components/admin-market-data/asset-profile-dialog/asset-profile-dialog.html</context>
<context context-type="linenumber">733</context>
<context context-type="linenumber">731</context>
</context-group>
</trans-unit>
<trans-unit id="2570446216260149991" datatype="html">
@ -6349,7 +6349,7 @@
<target state="translated">投资</target>
<context-group purpose="location">
<context context-type="sourcefile">apps/client/src/app/pages/portfolio/analysis/analysis-page.component.ts</context>
<context context-type="linenumber">109</context>
<context context-type="linenumber">104</context>
</context-group>
<context-group purpose="location">
<context context-type="sourcefile">apps/client/src/app/pages/resources/personal-finance-tools/product-page.component.ts</context>
@ -6542,7 +6542,7 @@
<target state="translated">数据收集</target>
<context-group purpose="location">
<context context-type="sourcefile">apps/client/src/app/components/admin-market-data/asset-profile-dialog/asset-profile-dialog.html</context>
<context context-type="linenumber">756</context>
<context context-type="linenumber">754</context>
</context-group>
<context-group purpose="location">
<context context-type="sourcefile">apps/client/src/app/components/admin-overview/admin-overview.html</context>
@ -7082,7 +7082,7 @@
</context-group>
<context-group purpose="location">
<context context-type="sourcefile">apps/client/src/app/components/admin-market-data/asset-profile-dialog/asset-profile-dialog.html</context>
<context context-type="linenumber">761</context>
<context context-type="linenumber">759</context>
</context-group>
<context-group purpose="location">
<context context-type="sourcefile">apps/client/src/app/components/admin-market-data/create-asset-profile-dialog/create-asset-profile-dialog.html</context>
@ -7134,7 +7134,7 @@
<target state="translated">关闭</target>
<context-group purpose="location">
<context context-type="sourcefile">apps/client/src/app/components/admin-market-data/asset-profile-dialog/asset-profile-dialog.html</context>
<context context-type="linenumber">763</context>
<context context-type="linenumber">761</context>
</context-group>
<context-group purpose="location">
<context context-type="sourcefile">apps/client/src/app/components/admin-market-data/create-asset-profile-dialog/create-asset-profile-dialog.html</context>
@ -7422,7 +7422,7 @@
<target state="new">Shares Before</target>
<context-group purpose="location">
<context context-type="sourcefile">apps/client/src/app/components/admin-market-data/asset-profile-dialog/asset-profile-dialog.html</context>
<context context-type="linenumber">605</context>
<context context-type="linenumber">603</context>
</context-group>
</trans-unit>
<trans-unit id="4605555143173907624" datatype="html">
@ -7712,7 +7712,7 @@
<target state="translated">保存</target>
<context-group purpose="location">
<context context-type="sourcefile">apps/client/src/app/components/admin-market-data/asset-profile-dialog/asset-profile-dialog.html</context>
<context context-type="linenumber">772</context>
<context context-type="linenumber">770</context>
</context-group>
<context-group purpose="location">
<context context-type="sourcefile">apps/client/src/app/components/admin-market-data/create-asset-profile-dialog/create-asset-profile-dialog.html</context>
@ -7764,7 +7764,7 @@
</context-group>
<context-group purpose="location">
<context context-type="sourcefile">apps/client/src/app/components/user-account-access/user-account-access.component.ts</context>
<context context-type="linenumber">255</context>
<context context-type="linenumber">257</context>
</context-group>
</trans-unit>
<trans-unit id="1769610706135259386" datatype="html">
@ -7812,7 +7812,7 @@
<target state="translated">AI 提示已复制到剪贴板</target>
<context-group purpose="location">
<context context-type="sourcefile">apps/client/src/app/pages/portfolio/analysis/analysis-page.component.ts</context>
<context context-type="linenumber">222</context>
<context context-type="linenumber">205</context>
</context-group>
</trans-unit>
<trans-unit id="1616747898909934803" datatype="html">
@ -7820,7 +7820,7 @@
<target state="translated">链接已复制到剪贴板</target>
<context-group purpose="location">
<context context-type="sourcefile">apps/client/src/app/components/access-table/access-table.component.ts</context>
<context context-type="linenumber">107</context>
<context context-type="linenumber">112</context>
</context-group>
</trans-unit>
<trans-unit id="8540986733881734625" datatype="html">
@ -7844,7 +7844,7 @@
<target state="translated">默认市场价格</target>
<context-group purpose="location">
<context context-type="sourcefile">apps/client/src/app/components/admin-market-data/asset-profile-dialog/asset-profile-dialog.html</context>
<context context-type="linenumber">646</context>
<context context-type="linenumber">644</context>
</context-group>
</trans-unit>
<trans-unit id="1713271461473302108" datatype="html">
@ -7852,7 +7852,7 @@
<target state="translated">模式</target>
<context-group purpose="location">
<context context-type="sourcefile">apps/client/src/app/components/admin-market-data/asset-profile-dialog/asset-profile-dialog.html</context>
<context context-type="linenumber">683</context>
<context context-type="linenumber">681</context>
</context-group>
</trans-unit>
<trans-unit id="1717822775546188057" datatype="html">
@ -7868,7 +7868,7 @@
<target state="translated">选择器</target>
<context-group purpose="location">
<context context-type="sourcefile">apps/client/src/app/components/admin-market-data/asset-profile-dialog/asset-profile-dialog.html</context>
<context context-type="linenumber">699</context>
<context context-type="linenumber">697</context>
</context-group>
</trans-unit>
<trans-unit id="645724892732039501" datatype="html">
@ -7876,7 +7876,7 @@
<target state="translated">HTTP 请求标头</target>
<context-group purpose="location">
<context context-type="sourcefile">apps/client/src/app/components/admin-market-data/asset-profile-dialog/asset-profile-dialog.html</context>
<context context-type="linenumber">659</context>
<context context-type="linenumber">657</context>
</context-group>
</trans-unit>
<trans-unit id="8635324470284879211" datatype="html">
@ -7900,7 +7900,7 @@
<target state="translated">打开 Duck.ai</target>
<context-group purpose="location">
<context context-type="sourcefile">apps/client/src/app/pages/portfolio/analysis/analysis-page.component.ts</context>
<context context-type="linenumber">223</context>
<context context-type="linenumber">206</context>
</context-group>
</trans-unit>
<trans-unit id="5674286808255988565" datatype="html">

3
libs/common/src/lib/dtos/create-access.dto.ts

@ -16,6 +16,9 @@ export class CreateAccessDto {
@IsUUID()
granteeUserId?: string;
/**
* @deprecated Use the scopes instead
*/
@IsEnum(AccessPermission, { each: true })
@IsOptional()
permissions?: AccessPermission[];

3
libs/common/src/lib/dtos/update-access.dto.ts

@ -19,6 +19,9 @@ export class UpdateAccessDto {
@IsString()
id: string;
/**
* @deprecated Use the scopes instead
*/
@IsEnum(AccessPermission, { each: true })
@IsOptional()
permissions?: AccessPermission[];

6
libs/common/src/lib/interfaces/access.interface.ts

@ -8,7 +8,13 @@ export interface Access {
alias: string | null;
grantee?: string;
id: string;
/**
* @deprecated Use the scopes instead
*/
permissions: AccessPermission[];
scopes: string[];
settings?: AccessSettings;
type: AccessType;
}

3
libs/common/src/lib/interfaces/user.interface.ts

@ -10,13 +10,14 @@ import { UserSettings } from './user-settings.interface';
// TODO: Compare with UserWithSettings
export interface User {
access: Pick<Access, 'alias' | 'id' | 'permissions'>[];
access: Pick<Access, 'alias' | 'id' | 'permissions' | 'scopes'>[];
accounts: AccountWithPlatform[];
activitiesCount: number;
dateOfFirstActivity: Date;
id: string;
permissions: string[];
referralPartners?: ReferralPartner[];
scopes: string[];
settings: UserSettings;
systemMessage?: SystemMessage;
subscription: {

25
libs/common/src/lib/permissions.ts

@ -1,6 +1,6 @@
import { UserWithSettings } from '@ghostfolio/common/types';
import { Access, Role } from '@prisma/client';
import { Role } from '@prisma/client';
export const permissions = {
accessAdminControl: 'accessAdminControl',
@ -138,7 +138,9 @@ export function getPermissions(aRole: Role): string[] {
permissions.createAccount,
permissions.createAccountBalance,
permissions.createActivity,
permissions.createAssetProfileSplitOfOwnAssetProfile,
// TODO: Grant createAssetProfileSplitOfOwnAssetProfile and
// deleteAssetProfileSplitOfOwnAssetProfile once the stock splits
// feature is no longer experimental
permissions.createMarketDataOfOwnAssetProfile,
permissions.createOwnTag,
permissions.createWatchlistItem,
@ -146,7 +148,6 @@ export function getPermissions(aRole: Role): string[] {
permissions.deleteAccount,
permissions.deleteAccountBalance,
permissions.deleteActivity,
permissions.deleteAssetProfileSplitOfOwnAssetProfile,
permissions.deleteAuthDevice,
permissions.deleteWatchlistItem,
permissions.readAiPrompt,
@ -197,24 +198,6 @@ export function hasPermission(
return aPermissions.includes(aPermission);
}
export function hasReadRestrictedAccessPermission({
accesses = [],
impersonationId
}: {
accesses?: Pick<Access, 'id' | 'permissions'>[];
impersonationId: string | null;
}) {
if (!impersonationId) {
return false;
}
const access = accesses.find(({ id }) => {
return id === impersonationId;
});
return access?.permissions?.includes('READ_RESTRICTED') ?? true;
}
export function hasRole(aUser: UserWithSettings, aRole: Role) {
return aUser?.role === aRole;
}

130
libs/common/src/lib/scopes.spec.ts

@ -0,0 +1,130 @@
import {
getScopesOfAccess,
getScopesOfOwnAccess,
getScopesOfUnrestrictedImpersonation,
hasScope,
scopes
} from '@ghostfolio/common/scopes';
describe('Scopes', () => {
describe('Get scopes of access', () => {
it('Scopes take precedence over the permissions', () => {
expect(
getScopesOfAccess({
granteeUserId: 'ffb08949-2f8a-4b6e-88fd-0f1e6b6b5f5d',
permissions: ['READ'],
scopes: [scopes.portfolioRead]
})
).toEqual([scopes.portfolioRead]);
});
it('Derive from the permission to read', () => {
// An access created before the scopes have been introduced has no scopes
expect(
getScopesOfAccess({
granteeUserId: 'ffb08949-2f8a-4b6e-88fd-0f1e6b6b5f5d',
permissions: ['READ'],
scopes: []
})
).toContain(scopes.portfolioReadValues);
});
it('Derive from the permission to read restricted', () => {
expect(
getScopesOfAccess({
granteeUserId: 'ffb08949-2f8a-4b6e-88fd-0f1e6b6b5f5d',
permissions: ['READ_RESTRICTED'],
scopes: []
})
).not.toContain(scopes.portfolioReadValues);
});
it('Without permissions and scopes', () => {
expect(
getScopesOfAccess({
granteeUserId: 'ffb08949-2f8a-4b6e-88fd-0f1e6b6b5f5d'
})
).not.toContain(scopes.portfolioReadValues);
});
});
describe('Get scopes of public access', () => {
it('Allows reading the portfolio', () => {
expect(getScopesOfAccess({ permissions: ['READ_RESTRICTED'] })).toContain(
scopes.portfolioRead
);
});
it('Excludes the accounts and the watchlist', () => {
const scopesOfAccess = getScopesOfAccess({
permissions: ['READ_RESTRICTED']
});
expect(scopesOfAccess).not.toContain(scopes.accountRead);
expect(scopesOfAccess).not.toContain(scopes.watchlistRead);
});
it('Cannot be widened by the scopes', () => {
expect(
getScopesOfAccess({
scopes: [
scopes.portfolioRead,
scopes.portfolioReadValues,
scopes.watchlistRead
]
})
).toEqual([scopes.portfolioRead]);
});
it('Cannot be widened by the permission to read', () => {
expect(getScopesOfAccess({ permissions: ['READ'] })).not.toContain(
scopes.portfolioReadValues
);
});
});
describe('Get scopes of own access', () => {
// A new scope has to be added here deliberately to confirm that it is
// granted to the owner of the data
it('Covers every scope', () => {
expect(getScopesOfOwnAccess()).toEqual([
scopes.accountRead,
scopes.activityRead,
scopes.portfolioRead,
scopes.portfolioReadValues,
scopes.watchlistRead
]);
});
});
describe('Get scopes of unrestricted impersonation', () => {
// A new scope has to be added here deliberately to confirm that it is
// granted to an administrator impersonating an arbitrary user
it('Covers every scope but the monetary values', () => {
expect(getScopesOfUnrestrictedImpersonation()).toEqual([
scopes.accountRead,
scopes.activityRead,
scopes.portfolioRead,
scopes.watchlistRead
]);
});
});
describe('Has scope', () => {
it('Present scope', () => {
expect(hasScope([scopes.portfolioRead], scopes.portfolioRead)).toEqual(
true
);
});
it('Absent scope', () => {
expect(
hasScope([scopes.portfolioRead], scopes.portfolioReadValues)
).toEqual(false);
});
it('Without scopes', () => {
expect(hasScope(undefined, scopes.portfolioRead)).toEqual(false);
});
});
});

78
libs/common/src/lib/scopes.ts

@ -0,0 +1,78 @@
import { AccessPermission } from '@prisma/client';
/**
* Scopes describe what a grantee may do on behalf of the granting user. They
* are a separate axis from the permissions, which describe the capabilities of
* a role. Both are evaluated, hence a delegation can only narrow the access of
* the authenticated user and never widen it.
*/
export const scopes = {
accountRead: 'account:read',
activityRead: 'activity:read',
portfolioRead: 'portfolio:read',
portfolioReadValues: 'portfolio:read:values',
watchlistRead: 'watchlist:read'
} as const;
export type Scope = (typeof scopes)[keyof typeof scopes];
const SCOPES_OF_PUBLIC_ACCESS: Scope[] = [
scopes.activityRead,
scopes.portfolioRead
];
const SCOPES_OF_READ_ACCESS = Object.values(scopes);
const SCOPES_OF_READ_RESTRICTED_ACCESS = SCOPES_OF_READ_ACCESS.filter(
(scope) => {
return scope !== scopes.portfolioReadValues;
}
);
export function getScopesOfAccess({
granteeUserId,
permissions,
scopes: scopesOfAccess
}: {
granteeUserId?: string | null;
permissions?: AccessPermission[];
scopes?: string[];
}): string[] {
if (!scopesOfAccess?.length) {
// TODO: Remove the derivation from the permissions once they have been
// dropped from the access
scopesOfAccess = permissions?.includes('READ')
? SCOPES_OF_READ_ACCESS
: SCOPES_OF_READ_RESTRICTED_ACCESS;
}
if (granteeUserId) {
return [...scopesOfAccess];
}
// An access which has not been granted to a user is public, hence it is
// narrowed to the scopes exposed by the public endpoints
return SCOPES_OF_PUBLIC_ACCESS.filter((scope) => {
return scopesOfAccess.includes(scope);
});
}
/**
* Scopes of a user acting on their own data, which is unrestricted. The
* permissions of the role are evaluated separately.
*/
export function getScopesOfOwnAccess(): string[] {
return Object.values(scopes);
}
/**
* Scopes of an administrator impersonating an arbitrary user, which excludes
* the monetary values
*/
export function getScopesOfUnrestrictedImpersonation(): string[] {
return [...SCOPES_OF_READ_RESTRICTED_ACCESS];
}
export function hasScope(aScopes: string[] = [], aScope: Scope) {
return aScopes.includes(aScope);
}

1
libs/common/src/lib/types/impersonation-context.type.ts

@ -8,6 +8,7 @@ import { UserSettings } from '@ghostfolio/common/interfaces';
export interface ImpersonationContext {
accessId?: string;
isActive: boolean;
scopes: string[];
userId: string;
userSettings: UserSettings;
}

3
libs/common/src/lib/types/user-with-settings.type.ts

@ -1,11 +1,10 @@
import { SubscriptionType } from '@ghostfolio/common/enums';
import { SubscriptionOffer, UserSettings } from '@ghostfolio/common/interfaces';
import { Access, Account, Settings, User } from '@prisma/client';
import { Account, Settings, User } from '@prisma/client';
// TODO: Compare with User interface
export type UserWithSettings = User & {
accessesGet: Access[];
accounts: Account[];
activityCount: number;
dataProviderGhostfolioDailyRequests: number;

2
prisma/migrations/20260815120000_added_symbol_profile_id_index_to_order/migration.sql

@ -0,0 +1,2 @@
-- CreateIndex
CREATE INDEX "Order_symbolProfileId_idx" ON "Order"("symbolProfileId");

24
prisma/migrations/20260815130000_added_scopes_to_access/migration.sql

@ -0,0 +1,24 @@
-- AlterTable
ALTER TABLE "Access" ADD COLUMN "scopes" TEXT[] DEFAULT ARRAY[]::TEXT[];
-- Derive the scopes from the permissions of the existing accesses
UPDATE "Access"
SET "scopes" = CASE
WHEN "granteeUserId" IS NULL THEN ARRAY[
'activity:read',
'portfolio:read'
]
WHEN 'READ' = ANY("permissions") THEN ARRAY[
'account:read',
'activity:read',
'portfolio:read',
'portfolio:read:values',
'watchlist:read'
]
ELSE ARRAY[
'account:read',
'activity:read',
'portfolio:read',
'watchlist:read'
]
END;

3
prisma/schema.prisma

@ -14,7 +14,9 @@ model Access {
granteeUser User? @relation("accessGet", fields: [granteeUserId], onDelete: Cascade, references: [id])
granteeUserId String?
id String @id @default(uuid())
/// @deprecated Use the scopes instead
permissions AccessPermission[] @default([READ_RESTRICTED])
scopes String[] @default([])
settings Json @default("{}")
updatedAt DateTime @updatedAt
userId String
@ -195,6 +197,7 @@ model Order {
@@index([accountId])
@@index([date])
@@index([symbolProfileId])
@@index([type])
@@index([userId])
}

Loading…
Cancel
Save