Compare commits
30 Commits
Author | SHA1 | Date | |
---|---|---|---|
0ec50819f5 | |||
c9abe818bc | |||
bfa32537a8 | |||
cef15afab8 | |||
1b9587c454 | |||
de76b0d8c3 | |||
e62989c981 | |||
d6b71e6314 | |||
8c59bfd6d7 | |||
f32df73256 | |||
9d03a8002c | |||
3c36ca29af | |||
efed7e3c2b | |||
b09d3cea95 | |||
eabd2f3934 | |||
cc184c2827 | |||
436f791fa4 | |||
e935a57dec | |||
203909d917 | |||
eed4f57f30 | |||
7878036bac | |||
75d140b436 | |||
a79f31b006 | |||
45cfd61dbb | |||
7fcfca952e | |||
279f16cc67 | |||
e7b1d8a5d3 | |||
1b2f8e5586 | |||
e4468252c6 | |||
ad3ebd42bb |
62
CHANGELOG.md
62
CHANGELOG.md
@ -5,6 +5,68 @@ 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).
|
||||
|
||||
## 1.234.0 - 2023-02-15
|
||||
|
||||
### Added
|
||||
|
||||
- Added the data import and export feature to the pricing page
|
||||
|
||||
### Changed
|
||||
|
||||
- Copy the logic of `GhostfolioScraperApiService` to `ManualService`
|
||||
- Improved the content of the landing page
|
||||
- Improved the content of the Frequently Asked Questions (FAQ) page
|
||||
- Improved the usability of the _Import Activities..._ action
|
||||
- Eliminated the permission `enableImport`
|
||||
- Set the exposed port as an environment variable (`PORT`) in `Dockerfile`
|
||||
- Migrated the style of `AboutPageModule` to `@angular/material` `15` (mdc)
|
||||
- Migrated the style of `BlogPageModule` to `@angular/material` `15` (mdc)
|
||||
- Migrated the style of `ChangelogPageModule` to `@angular/material` `15` (mdc)
|
||||
- Migrated the style of `ResourcesPageModule` to `@angular/material` `15` (mdc)
|
||||
- Upgraded `chart.js` from version `4.0.1` to `4.2.0`
|
||||
- Upgraded `ionicons` from version `6.0.4` to `6.1.2`
|
||||
- Upgraded `prettier` from version `2.8.1` to `2.8.4`
|
||||
- Upgraded `prisma` from version `4.9.0` to `4.10.1`
|
||||
|
||||
### Fixed
|
||||
|
||||
- Fixed an issue on the landing page caused by the global heat map of subscribers
|
||||
- Fixed the links in the interstitial for the subscription
|
||||
|
||||
### Todo
|
||||
|
||||
- Remove the environment variable `ENABLE_FEATURE_IMPORT`
|
||||
- Rename the `dataSource` from `GHOSTFOLIO` to `MANUAL`
|
||||
- Eliminate `GhostfolioScraperApiService`
|
||||
|
||||
## 1.233.0 - 2023-02-09
|
||||
|
||||
### Added
|
||||
|
||||
- Added support to export accounts
|
||||
- Added suport to import accounts
|
||||
|
||||
### Changed
|
||||
|
||||
- Improved the styling in the admin control panel
|
||||
- Removed the _Google Play_ badge from the landing page
|
||||
- Upgraded `eslint` dependencies
|
||||
|
||||
## 1.232.0 - 2023-02-05
|
||||
|
||||
### Changed
|
||||
|
||||
- Improved the language localization for German (`de`)
|
||||
- Migrated the style of `ActivitiesPageModule` to `@angular/material` `15` (mdc)
|
||||
- Migrated the style of `GfCreateOrUpdateActivityDialogModule` to `@angular/material` `15` (mdc)
|
||||
- Migrated the style of `GfMarketDataDetailDialogModule` to `@angular/material` `15` (mdc)
|
||||
- Upgraded `ng-extract-i18n-merge` from version `2.1.2` to `2.5.0`
|
||||
- Upgraded `ngx-markdown` from version `14.0.1` to `15.1.0`
|
||||
|
||||
### Fixed
|
||||
|
||||
- Fixed the `Upgrade Plan` button of the interstitial for the subscription
|
||||
|
||||
## 1.231.0 - 2023-02-04
|
||||
|
||||
### Added
|
||||
|
@ -57,5 +57,5 @@ RUN apt update && apt install -y \
|
||||
|
||||
COPY --from=builder /ghostfolio/dist/apps /ghostfolio/apps
|
||||
WORKDIR /ghostfolio/apps/api
|
||||
EXPOSE 3333
|
||||
EXPOSE ${PORT:-3333}
|
||||
CMD [ "yarn", "start:prod" ]
|
||||
|
@ -175,7 +175,7 @@ Run `yarn start:server`
|
||||
|
||||
### Start Client
|
||||
|
||||
Run `yarn start:client`
|
||||
Run `yarn start:client` and open http://localhost:4200/en in your browser
|
||||
|
||||
### Start _Storybook_
|
||||
|
||||
|
@ -1,5 +1,4 @@
|
||||
import { PortfolioService } from '@ghostfolio/api/app/portfolio/portfolio.service';
|
||||
import { UserService } from '@ghostfolio/api/app/user/user.service';
|
||||
import { RedactValuesInResponseInterceptor } from '@ghostfolio/api/interceptors/redact-values-in-response.interceptor';
|
||||
import { ImpersonationService } from '@ghostfolio/api/services/impersonation.service';
|
||||
import { Accounts } from '@ghostfolio/common/interfaces';
|
||||
|
@ -17,6 +17,10 @@ export class CreateAccountDto {
|
||||
@IsString()
|
||||
currency: string;
|
||||
|
||||
@IsOptional()
|
||||
@IsString()
|
||||
id?: string;
|
||||
|
||||
@IsBoolean()
|
||||
@IsOptional()
|
||||
isExcluded?: boolean;
|
||||
|
@ -244,6 +244,7 @@ export class AdminService {
|
||||
Analytics: {
|
||||
select: {
|
||||
activityCount: true,
|
||||
country: true,
|
||||
updatedAt: true
|
||||
}
|
||||
},
|
||||
@ -277,6 +278,7 @@ export class AdminService {
|
||||
id,
|
||||
subscription,
|
||||
accountCount: _count.Account || 0,
|
||||
country: Analytics.country,
|
||||
lastActivity: Analytics.updatedAt,
|
||||
transactionCount: _count.Order || 0
|
||||
};
|
||||
|
@ -61,8 +61,10 @@ export class AuthService {
|
||||
|
||||
// Create new user if not found
|
||||
user = await this.userService.createUser({
|
||||
provider,
|
||||
thirdPartyId: principalId
|
||||
data: {
|
||||
provider,
|
||||
thirdPartyId: principalId
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@ -96,8 +98,10 @@ export class AuthService {
|
||||
|
||||
// Create new user if not found
|
||||
user = await this.userService.createUser({
|
||||
provider,
|
||||
thirdPartyId
|
||||
data: {
|
||||
provider,
|
||||
thirdPartyId
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
|
@ -14,6 +14,22 @@ export class ExportService {
|
||||
activityIds?: string[];
|
||||
userId: string;
|
||||
}): Promise<Export> {
|
||||
const accounts = await this.prismaService.account.findMany({
|
||||
orderBy: {
|
||||
name: 'asc'
|
||||
},
|
||||
select: {
|
||||
accountType: true,
|
||||
balance: true,
|
||||
currency: true,
|
||||
id: true,
|
||||
isExcluded: true,
|
||||
name: true,
|
||||
platformId: true
|
||||
},
|
||||
where: { userId }
|
||||
});
|
||||
|
||||
let activities = await this.prismaService.order.findMany({
|
||||
orderBy: { date: 'desc' },
|
||||
select: {
|
||||
@ -38,6 +54,7 @@ export class ExportService {
|
||||
|
||||
return {
|
||||
meta: { date: new Date().toISOString(), version: environment.version },
|
||||
accounts,
|
||||
activities: activities.map(
|
||||
({
|
||||
accountId,
|
||||
|
@ -1,8 +1,15 @@
|
||||
import { CreateAccountDto } from '@ghostfolio/api/app/account/create-account.dto';
|
||||
import { CreateOrderDto } from '@ghostfolio/api/app/order/create-order.dto';
|
||||
import { Type } from 'class-transformer';
|
||||
import { IsArray, ValidateNested } from 'class-validator';
|
||||
import { IsArray, IsOptional, ValidateNested } from 'class-validator';
|
||||
|
||||
export class ImportDataDto {
|
||||
@IsOptional()
|
||||
@IsArray()
|
||||
@Type(() => CreateAccountDto)
|
||||
@ValidateNested({ each: true })
|
||||
accounts: CreateAccountDto[];
|
||||
|
||||
@IsArray()
|
||||
@Type(() => CreateOrderDto)
|
||||
@ValidateNested({ each: true })
|
||||
|
@ -2,6 +2,7 @@ import { TransformDataSourceInRequestInterceptor } from '@ghostfolio/api/interce
|
||||
import { TransformDataSourceInResponseInterceptor } from '@ghostfolio/api/interceptors/transform-data-source-in-response.interceptor';
|
||||
import { ConfigurationService } from '@ghostfolio/api/services/configuration.service';
|
||||
import { ImportResponse } from '@ghostfolio/common/interfaces';
|
||||
import { hasPermission, permissions } from '@ghostfolio/common/permissions';
|
||||
import type { RequestWithUser } from '@ghostfolio/common/types';
|
||||
import {
|
||||
Body,
|
||||
@ -38,7 +39,13 @@ export class ImportController {
|
||||
@Body() importData: ImportDataDto,
|
||||
@Query('dryRun') isDryRun?: boolean
|
||||
): Promise<ImportResponse> {
|
||||
if (!this.configurationService.get('ENABLE_FEATURE_IMPORT')) {
|
||||
if (
|
||||
!hasPermission(
|
||||
this.request.user.permissions,
|
||||
permissions.createAccount
|
||||
) ||
|
||||
!hasPermission(this.request.user.permissions, permissions.createOrder)
|
||||
) {
|
||||
throw new HttpException(
|
||||
getReasonPhrase(StatusCodes.FORBIDDEN),
|
||||
StatusCodes.FORBIDDEN
|
||||
@ -60,9 +67,10 @@ export class ImportController {
|
||||
|
||||
try {
|
||||
const activities = await this.importService.import({
|
||||
maxActivitiesToImport,
|
||||
isDryRun,
|
||||
maxActivitiesToImport,
|
||||
userCurrency,
|
||||
accountsDto: importData.accounts ?? [],
|
||||
activitiesDto: importData.activities,
|
||||
userId: this.request.user.id
|
||||
});
|
||||
|
@ -1,4 +1,5 @@
|
||||
import { AccountService } from '@ghostfolio/api/app/account/account.service';
|
||||
import { CreateAccountDto } from '@ghostfolio/api/app/account/create-account.dto';
|
||||
import { CreateOrderDto } from '@ghostfolio/api/app/order/create-order.dto';
|
||||
import { Activity } from '@ghostfolio/api/app/order/interfaces/activities.interface';
|
||||
import { OrderService } from '@ghostfolio/api/app/order/order.service';
|
||||
@ -100,18 +101,75 @@ export class ImportService {
|
||||
}
|
||||
|
||||
public async import({
|
||||
accountsDto,
|
||||
activitiesDto,
|
||||
isDryRun = false,
|
||||
maxActivitiesToImport,
|
||||
userCurrency,
|
||||
userId
|
||||
}: {
|
||||
accountsDto: Partial<CreateAccountDto>[];
|
||||
activitiesDto: Partial<CreateOrderDto>[];
|
||||
isDryRun?: boolean;
|
||||
maxActivitiesToImport: number;
|
||||
userCurrency: string;
|
||||
userId: string;
|
||||
}): Promise<Activity[]> {
|
||||
const accountIdMapping: { [oldAccountId: string]: string } = {};
|
||||
|
||||
if (!isDryRun && accountsDto?.length) {
|
||||
const existingAccounts = await this.accountService.accounts({
|
||||
where: {
|
||||
id: {
|
||||
in: accountsDto.map(({ id }) => {
|
||||
return id;
|
||||
})
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
for (const account of accountsDto) {
|
||||
// Check if there is any existing account with the same ID
|
||||
const accountWithSameId = existingAccounts.find(
|
||||
(existingAccount) => existingAccount.id === account.id
|
||||
);
|
||||
|
||||
// If there is no account or if the account belongs to a different user then create a new account
|
||||
if (!accountWithSameId || accountWithSameId.userId !== userId) {
|
||||
let oldAccountId: string;
|
||||
const platformId = account.platformId;
|
||||
|
||||
delete account.platformId;
|
||||
|
||||
if (accountWithSameId) {
|
||||
oldAccountId = account.id;
|
||||
delete account.id;
|
||||
}
|
||||
|
||||
const newAccountObject = {
|
||||
...account,
|
||||
User: { connect: { id: userId } }
|
||||
};
|
||||
|
||||
if (platformId) {
|
||||
Object.assign(newAccountObject, {
|
||||
Platform: { connect: { id: platformId } }
|
||||
});
|
||||
}
|
||||
|
||||
const newAccount = await this.accountService.createAccount(
|
||||
newAccountObject,
|
||||
userId
|
||||
);
|
||||
|
||||
// Store the new to old account ID mappings for updating activities
|
||||
if (accountWithSameId && oldAccountId) {
|
||||
accountIdMapping[oldAccountId] = newAccount.id;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
for (const activity of activitiesDto) {
|
||||
if (!activity.dataSource) {
|
||||
if (activity.type === 'ITEM') {
|
||||
@ -120,6 +178,13 @@ export class ImportService {
|
||||
activity.dataSource = this.dataProviderService.getPrimaryDataSource();
|
||||
}
|
||||
}
|
||||
|
||||
// If a new account is created, then update the accountId in all activities
|
||||
if (!isDryRun) {
|
||||
if (Object.keys(accountIdMapping).includes(activity.accountId)) {
|
||||
activity.accountId = accountIdMapping[activity.accountId];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const assetProfiles = await this.validateActivities({
|
||||
@ -128,12 +193,18 @@ export class ImportService {
|
||||
userId
|
||||
});
|
||||
|
||||
const accountIds = (await this.accountService.getAccounts(userId)).map(
|
||||
const accounts = (await this.accountService.getAccounts(userId)).map(
|
||||
(account) => {
|
||||
return account.id;
|
||||
return { id: account.id, name: account.name };
|
||||
}
|
||||
);
|
||||
|
||||
if (isDryRun) {
|
||||
accountsDto.forEach(({ id, name }) => {
|
||||
accounts.push({ id, name });
|
||||
});
|
||||
}
|
||||
|
||||
const activities: Activity[] = [];
|
||||
|
||||
for (const {
|
||||
@ -149,11 +220,15 @@ export class ImportService {
|
||||
unitPrice
|
||||
} of activitiesDto) {
|
||||
const date = parseISO(<string>(<unknown>dateString));
|
||||
const validatedAccountId = accountIds.includes(accountId)
|
||||
? accountId
|
||||
: undefined;
|
||||
const validatedAccount = accounts.find(({ id }) => {
|
||||
return id === accountId;
|
||||
});
|
||||
|
||||
let order: OrderWithAccount;
|
||||
let order:
|
||||
| OrderWithAccount
|
||||
| (Omit<OrderWithAccount, 'Account'> & {
|
||||
Account?: { id: string; name: string };
|
||||
});
|
||||
|
||||
if (isDryRun) {
|
||||
order = {
|
||||
@ -164,7 +239,7 @@ export class ImportService {
|
||||
type,
|
||||
unitPrice,
|
||||
userId,
|
||||
accountId: validatedAccountId,
|
||||
accountId: validatedAccount?.id,
|
||||
accountUserId: undefined,
|
||||
createdAt: new Date(),
|
||||
id: uuidv4(),
|
||||
@ -187,6 +262,7 @@ export class ImportService {
|
||||
url: null,
|
||||
...assetProfiles[symbol]
|
||||
},
|
||||
Account: validatedAccount,
|
||||
symbolProfileId: undefined,
|
||||
updatedAt: new Date()
|
||||
};
|
||||
@ -199,7 +275,7 @@ export class ImportService {
|
||||
type,
|
||||
unitPrice,
|
||||
userId,
|
||||
accountId: validatedAccountId,
|
||||
accountId: validatedAccount?.id,
|
||||
SymbolProfile: {
|
||||
connectOrCreate: {
|
||||
create: {
|
||||
@ -221,6 +297,7 @@ export class ImportService {
|
||||
|
||||
const value = new Big(quantity).mul(unitPrice).toNumber();
|
||||
|
||||
//@ts-ignore
|
||||
activities.push({
|
||||
...order,
|
||||
value,
|
||||
|
@ -72,10 +72,6 @@ export class InfoService {
|
||||
globalPermissions.push(permissions.enableFearAndGreedIndex);
|
||||
}
|
||||
|
||||
if (this.configurationService.get('ENABLE_FEATURE_IMPORT')) {
|
||||
globalPermissions.push(permissions.enableImport);
|
||||
}
|
||||
|
||||
if (this.configurationService.get('ENABLE_FEATURE_READ_ONLY_MODE')) {
|
||||
isReadOnlyMode = (await this.propertyService.getByKey(
|
||||
PROPERTY_IS_READ_ONLY_MODE
|
||||
|
7
apps/api/src/app/user/create-user.dto.ts
Normal file
7
apps/api/src/app/user/create-user.dto.ts
Normal file
@ -0,0 +1,7 @@
|
||||
import { IsOptional, IsString } from 'class-validator';
|
||||
|
||||
export class CreateUserDto {
|
||||
@IsString()
|
||||
@IsOptional()
|
||||
country?: string;
|
||||
}
|
@ -22,6 +22,7 @@ import { User as UserModel } from '@prisma/client';
|
||||
import { StatusCodes, getReasonPhrase } from 'http-status-codes';
|
||||
import { size } from 'lodash';
|
||||
|
||||
import { CreateUserDto } from './create-user.dto';
|
||||
import { UserItem } from './interfaces/user-item.interface';
|
||||
import { UpdateUserSettingDto } from './update-user-setting.dto';
|
||||
import { UserService } from './user.service';
|
||||
@ -65,7 +66,7 @@ export class UserController {
|
||||
}
|
||||
|
||||
@Post()
|
||||
public async signupUser(): Promise<UserItem> {
|
||||
public async signupUser(@Body() data: CreateUserDto): Promise<UserItem> {
|
||||
const isUserSignupEnabled =
|
||||
await this.propertyService.isUserSignupEnabled();
|
||||
|
||||
@ -79,7 +80,8 @@ export class UserController {
|
||||
const hasAdmin = await this.userService.hasAdmin();
|
||||
|
||||
const { accessToken, id, role } = await this.userService.createUser({
|
||||
role: hasAdmin ? 'USER' : 'ADMIN'
|
||||
country: data.country,
|
||||
data: { role: hasAdmin ? 'USER' : 'ADMIN' }
|
||||
});
|
||||
|
||||
return {
|
||||
|
@ -18,6 +18,8 @@ import { Injectable } from '@nestjs/common';
|
||||
import { Prisma, Role, User } from '@prisma/client';
|
||||
import { sortBy } from 'lodash';
|
||||
|
||||
import { CreateUserDto } from './create-user.dto';
|
||||
|
||||
const crypto = require('crypto');
|
||||
|
||||
@Injectable()
|
||||
@ -231,7 +233,10 @@ export class UserService {
|
||||
return hash.digest('hex');
|
||||
}
|
||||
|
||||
public async createUser(data: Prisma.UserCreateInput): Promise<User> {
|
||||
public async createUser({
|
||||
country,
|
||||
data
|
||||
}: CreateUserDto & { data: Prisma.UserCreateInput }): Promise<User> {
|
||||
if (!data?.provider) {
|
||||
data.provider = 'ANONYMOUS';
|
||||
}
|
||||
@ -256,6 +261,15 @@ export class UserService {
|
||||
}
|
||||
});
|
||||
|
||||
if (this.configurationService.get('ENABLE_FEATURE_SUBSCRIPTION')) {
|
||||
await this.prismaService.analytics.create({
|
||||
data: {
|
||||
country,
|
||||
User: { connect: { id: user.id } }
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
if (data.provider === 'ANONYMOUS') {
|
||||
const accessToken = this.createAccessToken(
|
||||
user.id,
|
||||
|
@ -24,7 +24,6 @@ export class ConfigurationService {
|
||||
ENABLE_FEATURE_BLOG: bool({ default: false }),
|
||||
ENABLE_FEATURE_CUSTOM_SYMBOLS: bool({ default: false }),
|
||||
ENABLE_FEATURE_FEAR_AND_GREED_INDEX: bool({ default: false }),
|
||||
ENABLE_FEATURE_IMPORT: bool({ default: true }),
|
||||
ENABLE_FEATURE_READ_ONLY_MODE: bool({ default: false }),
|
||||
ENABLE_FEATURE_SOCIAL_LOGIN: bool({ default: false }),
|
||||
ENABLE_FEATURE_STATISTICS: bool({ default: false }),
|
||||
|
@ -65,7 +65,7 @@ export class GhostfolioScraperApiService implements DataProviderInterface {
|
||||
const [symbolProfile] =
|
||||
await this.symbolProfileService.getSymbolProfilesBySymbols([symbol]);
|
||||
const { defaultMarketPrice, selector, url } =
|
||||
symbolProfile.scraperConfiguration;
|
||||
symbolProfile.scraperConfiguration ?? {};
|
||||
|
||||
if (defaultMarketPrice) {
|
||||
const historical: {
|
||||
@ -148,7 +148,7 @@ export class GhostfolioScraperApiService implements DataProviderInterface {
|
||||
dataSource: this.getName(),
|
||||
marketPrice: marketData.find((marketDataItem) => {
|
||||
return marketDataItem.symbol === symbolProfile.symbol;
|
||||
}).marketPrice,
|
||||
})?.marketPrice,
|
||||
marketState: 'delayed'
|
||||
};
|
||||
}
|
||||
|
@ -6,9 +6,17 @@ import {
|
||||
} from '@ghostfolio/api/services/interfaces/interfaces';
|
||||
import { PrismaService } from '@ghostfolio/api/services/prisma.service';
|
||||
import { SymbolProfileService } from '@ghostfolio/api/services/symbol-profile.service';
|
||||
import {
|
||||
DATE_FORMAT,
|
||||
extractNumberFromString,
|
||||
getYesterday
|
||||
} from '@ghostfolio/common/helper';
|
||||
import { Granularity } from '@ghostfolio/common/types';
|
||||
import { Injectable, Logger } from '@nestjs/common';
|
||||
import { DataSource, SymbolProfile } from '@prisma/client';
|
||||
import bent from 'bent';
|
||||
import * as cheerio from 'cheerio';
|
||||
import { addDays, format, isBefore } from 'date-fns';
|
||||
|
||||
@Injectable()
|
||||
export class ManualService implements DataProviderInterface {
|
||||
@ -18,7 +26,7 @@ export class ManualService implements DataProviderInterface {
|
||||
) {}
|
||||
|
||||
public canHandle(symbol: string) {
|
||||
return false;
|
||||
return true;
|
||||
}
|
||||
|
||||
public async getAssetProfile(
|
||||
@ -51,7 +59,57 @@ export class ManualService implements DataProviderInterface {
|
||||
): Promise<{
|
||||
[symbol: string]: { [date: string]: IDataProviderHistoricalResponse };
|
||||
}> {
|
||||
return {};
|
||||
try {
|
||||
const symbol = aSymbol;
|
||||
|
||||
const [symbolProfile] =
|
||||
await this.symbolProfileService.getSymbolProfilesBySymbols([symbol]);
|
||||
const { defaultMarketPrice, selector, url } =
|
||||
symbolProfile.scraperConfiguration ?? {};
|
||||
|
||||
if (defaultMarketPrice) {
|
||||
const historical: {
|
||||
[symbol: string]: { [date: string]: IDataProviderHistoricalResponse };
|
||||
} = {
|
||||
[symbol]: {}
|
||||
};
|
||||
let date = from;
|
||||
|
||||
while (isBefore(date, to)) {
|
||||
historical[symbol][format(date, DATE_FORMAT)] = {
|
||||
marketPrice: defaultMarketPrice
|
||||
};
|
||||
|
||||
date = addDays(date, 1);
|
||||
}
|
||||
|
||||
return historical;
|
||||
} else if (selector === undefined || url === undefined) {
|
||||
return {};
|
||||
}
|
||||
|
||||
const get = bent(url, 'GET', 'string', 200, {});
|
||||
|
||||
const html = await get();
|
||||
const $ = cheerio.load(html);
|
||||
|
||||
const value = extractNumberFromString($(selector).text());
|
||||
|
||||
return {
|
||||
[symbol]: {
|
||||
[format(getYesterday(), DATE_FORMAT)]: {
|
||||
marketPrice: value
|
||||
}
|
||||
}
|
||||
};
|
||||
} catch (error) {
|
||||
throw new Error(
|
||||
`Could not get historical market data for ${aSymbol} (${this.getName()}) from ${format(
|
||||
from,
|
||||
DATE_FORMAT
|
||||
)} to ${format(to, DATE_FORMAT)}: [${error.name}] ${error.message}`
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
public getName(): DataSource {
|
||||
@ -88,10 +146,9 @@ export class ManualService implements DataProviderInterface {
|
||||
response[symbolProfile.symbol] = {
|
||||
currency: symbolProfile.currency,
|
||||
dataSource: this.getName(),
|
||||
marketPrice:
|
||||
marketData.find((marketDataItem) => {
|
||||
return marketDataItem.symbol === symbolProfile.symbol;
|
||||
})?.marketPrice ?? 0,
|
||||
marketPrice: marketData.find((marketDataItem) => {
|
||||
return marketDataItem.symbol === symbolProfile.symbol;
|
||||
})?.marketPrice,
|
||||
marketState: 'delayed'
|
||||
};
|
||||
}
|
||||
|
@ -10,7 +10,6 @@ export interface Environment extends CleanedEnvAccessors {
|
||||
ENABLE_FEATURE_BLOG: boolean;
|
||||
ENABLE_FEATURE_CUSTOM_SYMBOLS: boolean;
|
||||
ENABLE_FEATURE_FEAR_AND_GREED_INDEX: boolean;
|
||||
ENABLE_FEATURE_IMPORT: boolean;
|
||||
ENABLE_FEATURE_READ_ONLY_MODE: boolean;
|
||||
ENABLE_FEATURE_SOCIAL_LOGIN: boolean;
|
||||
ENABLE_FEATURE_STATISTICS: boolean;
|
||||
|
@ -40,7 +40,6 @@
|
||||
[hasPermissionToCreateActivity]="false"
|
||||
[hasPermissionToExportActivities]="true"
|
||||
[hasPermissionToFilter]="false"
|
||||
[hasPermissionToImportActivities]="false"
|
||||
[hasPermissionToOpenDetails]="false"
|
||||
[locale]="user?.settings?.locale"
|
||||
[showActions]="false"
|
||||
|
@ -1,6 +1,6 @@
|
||||
<form class="d-flex flex-column h-100">
|
||||
<h1 i18n mat-dialog-title>Details for {{ data.symbol }}</h1>
|
||||
<div class="flex-grow-1" mat-dialog-content>
|
||||
<div class="flex-grow-1 pt-3" mat-dialog-content>
|
||||
<div class="mb-3">
|
||||
<mat-form-field appearance="outline" class="w-100">
|
||||
<mat-label i18n>Date</mat-label>
|
||||
@ -11,7 +11,7 @@
|
||||
[matDatepicker]="date"
|
||||
[(ngModel)]="data.date"
|
||||
/>
|
||||
<mat-datepicker-toggle matSuffix [for]="date">
|
||||
<mat-datepicker-toggle class="mr-2" matSuffix [for]="date">
|
||||
<ion-icon
|
||||
class="text-muted"
|
||||
matDatepickerToggleIcon
|
||||
@ -21,7 +21,7 @@
|
||||
<mat-datepicker #date disabled="true"></mat-datepicker>
|
||||
</mat-form-field>
|
||||
</div>
|
||||
<div>
|
||||
<div class="align-items-start d-flex">
|
||||
<mat-form-field appearance="outline" class="w-100">
|
||||
<mat-label i18n>Market Price</mat-label>
|
||||
<input
|
||||
@ -30,16 +30,16 @@
|
||||
type="number"
|
||||
[(ngModel)]="data.marketPrice"
|
||||
/>
|
||||
<span class="ml-2" matSuffix>{{ data.currency }}</span>
|
||||
<button
|
||||
mat-icon-button
|
||||
matSuffix
|
||||
title="Fetch market price"
|
||||
(click)="onFetchSymbolForDate()"
|
||||
>
|
||||
<ion-icon class="text-muted" name="refresh-outline"></ion-icon>
|
||||
</button>
|
||||
<span class="ml-2" matTextSuffix>{{ data.currency }}</span>
|
||||
</mat-form-field>
|
||||
<button
|
||||
class="apply-current-market-price ml-2 no-min-width"
|
||||
mat-button
|
||||
title="Fetch market price"
|
||||
(click)="onFetchSymbolForDate()"
|
||||
>
|
||||
<ion-icon class="text-muted" name="refresh-outline"></ion-icon>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="justify-content-end" mat-dialog-actions>
|
||||
|
@ -2,10 +2,10 @@ import { CommonModule } from '@angular/common';
|
||||
import { CUSTOM_ELEMENTS_SCHEMA, NgModule } from '@angular/core';
|
||||
import { FormsModule, ReactiveFormsModule } from '@angular/forms';
|
||||
import { MatDatepickerModule } from '@angular/material/datepicker';
|
||||
import { MatLegacyButtonModule as MatButtonModule } from '@angular/material/legacy-button';
|
||||
import { MatButtonModule } from '@angular/material/button';
|
||||
import { MatLegacyDialogModule as MatDialogModule } from '@angular/material/legacy-dialog';
|
||||
import { MatLegacyFormFieldModule as MatFormFieldModule } from '@angular/material/legacy-form-field';
|
||||
import { MatLegacyInputModule as MatInputModule } from '@angular/material/legacy-input';
|
||||
import { MatFormFieldModule } from '@angular/material/form-field';
|
||||
import { MatInputModule } from '@angular/material/input';
|
||||
|
||||
import { MarketDataDetailDialog } from './market-data-detail-dialog.component';
|
||||
|
||||
|
@ -4,19 +4,9 @@
|
||||
.mat-dialog-content {
|
||||
max-height: unset;
|
||||
|
||||
.mat-form-field-appearance-outline {
|
||||
::ng-deep {
|
||||
.mat-form-field-suffix {
|
||||
top: -0.3rem;
|
||||
}
|
||||
|
||||
.mat-form-field-wrapper {
|
||||
padding-bottom: 0;
|
||||
}
|
||||
}
|
||||
|
||||
ion-icon {
|
||||
font-size: 130%;
|
||||
.mat-mdc-button {
|
||||
&.apply-current-market-price {
|
||||
height: 56px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -29,7 +29,7 @@ import { takeUntil } from 'rxjs/operators';
|
||||
templateUrl: './admin-overview.html'
|
||||
})
|
||||
export class AdminOverviewComponent implements OnDestroy, OnInit {
|
||||
public couponDuration: StringValue = '30 days';
|
||||
public couponDuration: StringValue = '14 days';
|
||||
public coupons: Coupon[];
|
||||
public customCurrencies: string[];
|
||||
public exchangeRates: { label1: string; label2: string; value: number }[];
|
||||
|
@ -151,16 +151,23 @@
|
||||
>
|
||||
<div class="w-50" i18n>Coupons</div>
|
||||
<div class="w-50">
|
||||
<div *ngFor="let coupon of coupons">
|
||||
<span>{{ coupon.code }} ({{ coupon.duration }})</span>
|
||||
<button
|
||||
class="mini-icon mx-1 no-min-width px-2"
|
||||
mat-button
|
||||
(click)="onDeleteCoupon(coupon.code)"
|
||||
>
|
||||
<ion-icon name="trash-outline"></ion-icon>
|
||||
</button>
|
||||
</div>
|
||||
<table>
|
||||
<tr *ngFor="let coupon of coupons">
|
||||
<td class="text-monospace">{{ coupon.code }}</td>
|
||||
<td class="d-flex justify-content-end pl-2">
|
||||
{{ coupon.duration }}
|
||||
</td>
|
||||
<td>
|
||||
<button
|
||||
class="mini-icon mx-1 no-min-width px-2"
|
||||
mat-button
|
||||
(click)="onDeleteCoupon(coupon.code)"
|
||||
>
|
||||
<ion-icon name="trash-outline"></ion-icon>
|
||||
</button>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
<div class="mt-2">
|
||||
<form #couponForm="ngForm" class="align-items-center d-flex">
|
||||
<mat-form-field
|
||||
|
@ -1,7 +1,9 @@
|
||||
import { ChangeDetectorRef, Component, OnDestroy, OnInit } from '@angular/core';
|
||||
import { DataService } from '@ghostfolio/client/services/data.service';
|
||||
import { UserService } from '@ghostfolio/client/services/user/user.service';
|
||||
import { AdminData, User } from '@ghostfolio/common/interfaces';
|
||||
import { getEmojiFlag } from '@ghostfolio/common/helper';
|
||||
import { AdminData, InfoItem, User } from '@ghostfolio/common/interfaces';
|
||||
import { hasPermission, permissions } from '@ghostfolio/common/permissions';
|
||||
import {
|
||||
differenceInSeconds,
|
||||
formatDistanceToNowStrict,
|
||||
@ -16,6 +18,9 @@ import { takeUntil } from 'rxjs/operators';
|
||||
templateUrl: './admin-users.html'
|
||||
})
|
||||
export class AdminUsersComponent implements OnDestroy, OnInit {
|
||||
public getEmojiFlag = getEmojiFlag;
|
||||
public hasPermissionForSubscription: boolean;
|
||||
public info: InfoItem;
|
||||
public user: User;
|
||||
public users: AdminData['users'];
|
||||
|
||||
@ -26,6 +31,13 @@ export class AdminUsersComponent implements OnDestroy, OnInit {
|
||||
private dataService: DataService,
|
||||
private userService: UserService
|
||||
) {
|
||||
this.info = this.dataService.fetchInfo();
|
||||
|
||||
this.hasPermissionForSubscription = hasPermission(
|
||||
this.info?.globalPermissions,
|
||||
permissions.enableSubscription
|
||||
);
|
||||
|
||||
this.userService.stateChanged
|
||||
.pipe(takeUntil(this.unsubscribeSubject))
|
||||
.subscribe((state) => {
|
||||
|
@ -7,7 +7,13 @@
|
||||
<tr class="mat-header-row">
|
||||
<th class="mat-header-cell px-1 py-2 text-right">#</th>
|
||||
<th class="mat-header-cell px-1 py-2" i18n>User</th>
|
||||
<th class="mat-header-cell px-1 py-2 text-right">
|
||||
<th
|
||||
*ngIf="hasPermissionForSubscription"
|
||||
class="mat-header-cell px-1 py-2"
|
||||
>
|
||||
<ng-container i18n>Country</ng-container>
|
||||
</th>
|
||||
<th class="mat-header-cell px-1 py-2">
|
||||
<ng-container i18n>Registration</ng-container>
|
||||
</th>
|
||||
<th class="mat-header-cell px-1 py-2 text-right">
|
||||
@ -16,7 +22,10 @@
|
||||
<th class="mat-header-cell px-1 py-2 text-right">
|
||||
<ng-container i18n>Activities</ng-container>
|
||||
</th>
|
||||
<th class="mat-header-cell px-1 py-2 text-right">
|
||||
<th
|
||||
*ngIf="hasPermissionForSubscription"
|
||||
class="mat-header-cell px-1 py-2 text-right"
|
||||
>
|
||||
<ng-container i18n>Engagement per Day</ng-container>
|
||||
</th>
|
||||
<th class="mat-header-cell px-1 py-2" i18n>Last Request</th>
|
||||
@ -28,10 +37,10 @@
|
||||
<td class="mat-cell px-1 py-2 text-right">{{ i + 1 }}</td>
|
||||
<td class="mat-cell px-1 py-2">
|
||||
<div class="d-flex align-items-center">
|
||||
<span class="d-none d-sm-inline-block"
|
||||
<span class="d-none d-sm-inline-block text-monospace"
|
||||
>{{ userItem.id }}</span
|
||||
>
|
||||
<span class="d-inline-block d-sm-none"
|
||||
<span class="d-inline-block d-sm-none text-monospace"
|
||||
>{{ (userItem.id | slice:0:5) + '...' }}</span
|
||||
>
|
||||
<gf-premium-indicator
|
||||
@ -41,7 +50,15 @@
|
||||
></gf-premium-indicator>
|
||||
</div>
|
||||
</td>
|
||||
<td class="mat-cell px-1 py-2 text-right">
|
||||
<td
|
||||
*ngIf="hasPermissionForSubscription"
|
||||
class="mat-cell px-1 py-2"
|
||||
>
|
||||
<span class="h5" [title]="userItem.country"
|
||||
>{{ getEmojiFlag(userItem.country) }}</span
|
||||
>
|
||||
</td>
|
||||
<td class="mat-cell px-1 py-2">
|
||||
{{ formatDistanceToNow(userItem.createdAt) }}
|
||||
</td>
|
||||
<td class="mat-cell px-1 py-2 text-right">
|
||||
@ -58,7 +75,10 @@
|
||||
[value]="userItem.transactionCount"
|
||||
></gf-value>
|
||||
</td>
|
||||
<td class="mat-cell px-1 py-2 text-right">
|
||||
<td
|
||||
*ngIf="hasPermissionForSubscription"
|
||||
class="mat-cell px-1 py-2 text-right"
|
||||
>
|
||||
<gf-value
|
||||
class="d-inline-block justify-content-end"
|
||||
[locale]="user?.settings?.locale"
|
||||
|
@ -27,6 +27,7 @@ import { ColorScheme } from '@ghostfolio/common/types';
|
||||
import { SymbolProfile } from '@prisma/client';
|
||||
import {
|
||||
Chart,
|
||||
ChartData,
|
||||
LineController,
|
||||
LineElement,
|
||||
LinearScale,
|
||||
@ -57,7 +58,7 @@ export class BenchmarkComparatorComponent implements OnChanges, OnDestroy {
|
||||
|
||||
@ViewChild('chartCanvas') chartCanvas;
|
||||
|
||||
public chart: Chart<any>;
|
||||
public chart: Chart<'line'>;
|
||||
|
||||
public constructor() {
|
||||
Chart.register(
|
||||
@ -89,14 +90,14 @@ export class BenchmarkComparatorComponent implements OnChanges, OnDestroy {
|
||||
}
|
||||
|
||||
private initialize() {
|
||||
const data = {
|
||||
const data: ChartData<'line'> = {
|
||||
datasets: [
|
||||
{
|
||||
backgroundColor: `rgb(${primaryColorRgb.r}, ${primaryColorRgb.g}, ${primaryColorRgb.b})`,
|
||||
borderColor: `rgb(${primaryColorRgb.r}, ${primaryColorRgb.g}, ${primaryColorRgb.b})`,
|
||||
borderWidth: 2,
|
||||
data: this.performanceDataItems.map(({ date, value }) => {
|
||||
return { x: parseDate(date), y: value };
|
||||
return { x: parseDate(date).getTime(), y: value };
|
||||
}),
|
||||
label: $localize`Portfolio`
|
||||
},
|
||||
@ -105,7 +106,7 @@ export class BenchmarkComparatorComponent implements OnChanges, OnDestroy {
|
||||
borderColor: `rgb(${secondaryColorRgb.r}, ${secondaryColorRgb.g}, ${secondaryColorRgb.b})`,
|
||||
borderWidth: 2,
|
||||
data: this.benchmarkDataItems.map(({ date, value }) => {
|
||||
return { x: parseDate(date), y: value };
|
||||
return { x: parseDate(date).getTime(), y: value };
|
||||
}),
|
||||
label: $localize`Benchmark`
|
||||
}
|
||||
|
@ -29,6 +29,7 @@ import {
|
||||
BarController,
|
||||
BarElement,
|
||||
Chart,
|
||||
ChartData,
|
||||
LineController,
|
||||
LineElement,
|
||||
LinearScale,
|
||||
@ -62,7 +63,7 @@ export class InvestmentChartComponent implements OnChanges, OnDestroy {
|
||||
|
||||
@ViewChild('chartCanvas') chartCanvas;
|
||||
|
||||
public chart: Chart<any>;
|
||||
public chart: Chart<'bar' | 'line'>;
|
||||
private investments: InvestmentItem[];
|
||||
private values: LineChartItem[];
|
||||
|
||||
@ -142,7 +143,7 @@ export class InvestmentChartComponent implements OnChanges, OnDestroy {
|
||||
});
|
||||
}
|
||||
|
||||
const chartData = {
|
||||
const chartData: ChartData<'line'> = {
|
||||
labels: this.historicalDataItems.map(({ date }) => {
|
||||
return parseDate(date);
|
||||
}),
|
||||
@ -153,7 +154,7 @@ export class InvestmentChartComponent implements OnChanges, OnDestroy {
|
||||
borderWidth: this.groupBy ? 0 : 1,
|
||||
data: this.investments.map(({ date, investment }) => {
|
||||
return {
|
||||
x: parseDate(date),
|
||||
x: parseDate(date).getTime(),
|
||||
y: this.isInPercent ? investment * 100 : investment
|
||||
};
|
||||
}),
|
||||
@ -173,7 +174,7 @@ export class InvestmentChartComponent implements OnChanges, OnDestroy {
|
||||
borderWidth: 2,
|
||||
data: this.values.map(({ date, value }) => {
|
||||
return {
|
||||
x: parseDate(date),
|
||||
x: parseDate(date).getTime(),
|
||||
y: this.isInPercent ? value * 100 : value
|
||||
};
|
||||
}),
|
||||
|
@ -239,7 +239,6 @@
|
||||
[hasPermissionToCreateActivity]="false"
|
||||
[hasPermissionToExportActivities]="true"
|
||||
[hasPermissionToFilter]="false"
|
||||
[hasPermissionToImportActivities]="false"
|
||||
[hasPermissionToOpenDetails]="false"
|
||||
[locale]="data.locale"
|
||||
[showActions]="false"
|
||||
|
@ -19,7 +19,7 @@ export class SubscriptionInterstitialDialog {
|
||||
public dialogRef: MatDialogRef<SubscriptionInterstitialDialog>
|
||||
) {}
|
||||
|
||||
public onCancel() {
|
||||
public closeDialog() {
|
||||
this.dialogRef.close({});
|
||||
}
|
||||
}
|
||||
|
@ -1,6 +1,9 @@
|
||||
<h1 class="align-items-center d-flex" mat-dialog-title>
|
||||
<span>Ghostfolio Premium</span>
|
||||
<gf-premium-indicator class="ml-1"></gf-premium-indicator>
|
||||
<gf-premium-indicator
|
||||
class="ml-1"
|
||||
[enableLink]="false"
|
||||
></gf-premium-indicator>
|
||||
</h1>
|
||||
<div class="flex-grow-1" mat-dialog-content>
|
||||
<p class="h5" i18n>
|
||||
@ -28,14 +31,19 @@
|
||||
</li>
|
||||
<li class="align-items-center d-flex mb-1">
|
||||
<ion-icon class="mr-1" name="checkmark-circle-outline"></ion-icon>
|
||||
<a i18n [routerLink]="['/features']">and more Features...</a>
|
||||
<span i18n>and more Features...</span>
|
||||
</li>
|
||||
</ul>
|
||||
<p>Refine your personal investment strategy now.</p>
|
||||
</div>
|
||||
<div class="justify-content-end" mat-dialog-actions>
|
||||
<button i18n mat-button (click)="onCancel()">Skip</button>
|
||||
<a color="primary" mat-flat-button [routerLink]="['/pricing']">
|
||||
<button i18n mat-button (click)="closeDialog()">Skip</button>
|
||||
<a
|
||||
color="primary"
|
||||
mat-flat-button
|
||||
[routerLink]="['/pricing']"
|
||||
(click)="closeDialog()"
|
||||
>
|
||||
<span i18n>Upgrade Plan</span>
|
||||
<ion-icon class="ml-1" name="arrow-forward-outline"></ion-icon>
|
||||
</a>
|
||||
|
@ -119,7 +119,7 @@
|
||||
<div *ngIf="hasPermissionForStatistics" class="mb-5 row">
|
||||
<div class="col">
|
||||
<h3 class="mb-3 text-center">Ghostfolio in Numbers</h3>
|
||||
<mat-card>
|
||||
<mat-card appearance="outlined">
|
||||
<mat-card-content>
|
||||
<div class="row">
|
||||
<div class="col-xs-12 col-md-4 my-2">
|
||||
|
@ -1,7 +1,7 @@
|
||||
import { CommonModule } from '@angular/common';
|
||||
import { CUSTOM_ELEMENTS_SCHEMA, NgModule } from '@angular/core';
|
||||
import { MatLegacyButtonModule as MatButtonModule } from '@angular/material/legacy-button';
|
||||
import { MatLegacyCardModule as MatCardModule } from '@angular/material/legacy-card';
|
||||
import { MatButtonModule } from '@angular/material/button';
|
||||
import { MatCardModule } from '@angular/material/card';
|
||||
import { GfValueModule } from '@ghostfolio/ui/value';
|
||||
|
||||
import { AboutPageRoutingModule } from './about-page-routing.module';
|
||||
|
@ -2,7 +2,7 @@
|
||||
<div class="mb-5 row">
|
||||
<div class="col">
|
||||
<h3 class="mb-3 text-center" i18n>Changelog</h3>
|
||||
<mat-card class="changelog">
|
||||
<mat-card appearance="outlined" class="changelog">
|
||||
<mat-card-content>
|
||||
<markdown [src]="'../assets/CHANGELOG.md'"></markdown>
|
||||
</mat-card-content>
|
||||
@ -13,7 +13,7 @@
|
||||
<div class="row">
|
||||
<div class="col">
|
||||
<h3 class="mb-3 text-center" i18n>License</h3>
|
||||
<mat-card>
|
||||
<mat-card appearance="outlined">
|
||||
<mat-card-content>
|
||||
<markdown [src]="'../assets/LICENSE'"></markdown>
|
||||
</mat-card-content>
|
||||
|
@ -1,6 +1,6 @@
|
||||
import { CommonModule } from '@angular/common';
|
||||
import { CUSTOM_ELEMENTS_SCHEMA, NgModule } from '@angular/core';
|
||||
import { MatLegacyCardModule as MatCardModule } from '@angular/material/legacy-card';
|
||||
import { MatCardModule } from '@angular/material/card';
|
||||
import { MarkdownModule } from 'ngx-markdown';
|
||||
|
||||
import { ChangelogPageRoutingModule } from './changelog-page-routing.module';
|
||||
|
@ -2,20 +2,18 @@
|
||||
color: rgb(var(--dark-primary-text));
|
||||
display: block;
|
||||
|
||||
.mat-card {
|
||||
&.changelog {
|
||||
a {
|
||||
color: rgba(var(--palette-primary-500), 1);
|
||||
font-weight: 500;
|
||||
|
||||
&:hover {
|
||||
color: rgba(var(--palette-primary-300), 1);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.mat-mdc-card {
|
||||
&.changelog {
|
||||
::ng-deep {
|
||||
a {
|
||||
color: rgba(var(--palette-primary-500), 1);
|
||||
font-weight: 500;
|
||||
|
||||
&:hover {
|
||||
color: rgba(var(--palette-primary-300), 1);
|
||||
}
|
||||
}
|
||||
|
||||
markdown {
|
||||
h1,
|
||||
p {
|
||||
|
@ -257,7 +257,7 @@
|
||||
</div>
|
||||
<div class="align-items-center d-flex mt-4 py-1">
|
||||
<div class="pr-1 w-50" i18n>User ID</div>
|
||||
<div class="pl-1 w-50">{{ user?.id }}</div>
|
||||
<div class="pl-1 text-monospace w-50">{{ user?.id }}</div>
|
||||
</div>
|
||||
</mat-card-content>
|
||||
</mat-card>
|
||||
|
@ -30,7 +30,7 @@
|
||||
[queryParams]="{ createDialog: true }"
|
||||
[routerLink]="[]"
|
||||
>
|
||||
<ion-icon name="add-outline" size="large"></ion-icon>
|
||||
<ion-icon class="mt-2" name="add-outline" size="large"></ion-icon>
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
|
@ -1,6 +1,6 @@
|
||||
import { CommonModule } from '@angular/common';
|
||||
import { CUSTOM_ELEMENTS_SCHEMA, NgModule } from '@angular/core';
|
||||
import { MatLegacyButtonModule as MatButtonModule } from '@angular/material/legacy-button';
|
||||
import { MatButtonModule } from '@angular/material/button';
|
||||
import { RouterModule } from '@angular/router';
|
||||
import { GfAccountDetailDialogModule } from '@ghostfolio/client/components/account-detail-dialog/account-detail-dialog.module';
|
||||
import { GfAccountsTableModule } from '@ghostfolio/client/components/accounts-table/accounts-table.module';
|
||||
|
@ -1,15 +1,11 @@
|
||||
import { CommonModule } from '@angular/common';
|
||||
import { CUSTOM_ELEMENTS_SCHEMA, NgModule } from '@angular/core';
|
||||
import { MatLegacyButtonModule as MatButtonModule } from '@angular/material/legacy-button';
|
||||
import { MatLegacyCardModule as MatCardModule } from '@angular/material/legacy-card';
|
||||
import { MatLegacyMenuModule as MatMenuModule } from '@angular/material/legacy-menu';
|
||||
import { MatLegacyTabsModule as MatTabsModule } from '@angular/material/legacy-tabs';
|
||||
import { GfAdminJobsModule } from '@ghostfolio/client/components/admin-jobs/admin-jobs.module';
|
||||
import { GfAdminMarketDataModule } from '@ghostfolio/client/components/admin-market-data/admin-market-data.module';
|
||||
import { GfAdminOverviewModule } from '@ghostfolio/client/components/admin-overview/admin-overview.module';
|
||||
import { GfAdminUsersModule } from '@ghostfolio/client/components/admin-users/admin-users.module';
|
||||
import { CacheService } from '@ghostfolio/client/services/cache.service';
|
||||
import { GfValueModule } from '@ghostfolio/ui/value';
|
||||
|
||||
import { AdminPageRoutingModule } from './admin-page-routing.module';
|
||||
import { AdminPageComponent } from './admin-page.component';
|
||||
@ -24,10 +20,6 @@ import { AdminPageComponent } from './admin-page.component';
|
||||
GfAdminMarketDataModule,
|
||||
GfAdminOverviewModule,
|
||||
GfAdminUsersModule,
|
||||
GfValueModule,
|
||||
MatButtonModule,
|
||||
MatCardModule,
|
||||
MatMenuModule,
|
||||
MatTabsModule
|
||||
],
|
||||
providers: [CacheService],
|
||||
|
@ -1,6 +1,6 @@
|
||||
import { CommonModule } from '@angular/common';
|
||||
import { CUSTOM_ELEMENTS_SCHEMA, NgModule } from '@angular/core';
|
||||
import { MatLegacyButtonModule as MatButtonModule } from '@angular/material/legacy-button';
|
||||
import { MatButtonModule } from '@angular/material/button';
|
||||
import { RouterModule } from '@angular/router';
|
||||
import { GfPremiumIndicatorModule } from '@ghostfolio/ui/premium-indicator';
|
||||
|
||||
|
@ -1,6 +1,6 @@
|
||||
import { CommonModule } from '@angular/common';
|
||||
import { CUSTOM_ELEMENTS_SCHEMA, NgModule } from '@angular/core';
|
||||
import { MatLegacyButtonModule as MatButtonModule } from '@angular/material/legacy-button';
|
||||
import { MatButtonModule } from '@angular/material/button';
|
||||
import { RouterModule } from '@angular/router';
|
||||
|
||||
import { TheImportanceOfTrackingYourPersonalFinancesRoutingModule } from './the-importance-of-tracking-your-personal-finances-page-routing.module';
|
||||
|
@ -2,7 +2,7 @@
|
||||
<div class="mb-5 row">
|
||||
<div class="col">
|
||||
<h3 class="d-none d-sm-block mb-3 text-center" i18n>Blog</h3>
|
||||
<mat-card class="mb-3">
|
||||
<mat-card appearance="outlined" class="mb-3">
|
||||
<mat-card-content>
|
||||
<div class="container p-0">
|
||||
<div class="flex-nowrap no-gutters row">
|
||||
@ -28,7 +28,7 @@
|
||||
</div>
|
||||
</mat-card-content>
|
||||
</mat-card>
|
||||
<mat-card class="mb-3">
|
||||
<mat-card appearance="outlined" class="mb-3">
|
||||
<mat-card-content>
|
||||
<div class="container p-0">
|
||||
<div class="flex-nowrap no-gutters row">
|
||||
@ -54,7 +54,11 @@
|
||||
</div>
|
||||
</mat-card-content>
|
||||
</mat-card>
|
||||
<mat-card *ngIf="hasPermissionForSubscription" class="mb-3">
|
||||
<mat-card
|
||||
*ngIf="hasPermissionForSubscription"
|
||||
appearance="outlined"
|
||||
class="mb-3"
|
||||
>
|
||||
<mat-card-content>
|
||||
<div class="container p-0">
|
||||
<div class="flex-nowrap no-gutters row">
|
||||
@ -78,7 +82,7 @@
|
||||
</div>
|
||||
</mat-card-content>
|
||||
</mat-card>
|
||||
<mat-card class="mb-3">
|
||||
<mat-card appearance="outlined" class="mb-3">
|
||||
<mat-card-content>
|
||||
<div class="container p-0">
|
||||
<div class="flex-nowrap no-gutters row">
|
||||
@ -102,7 +106,7 @@
|
||||
</div>
|
||||
</mat-card-content>
|
||||
</mat-card>
|
||||
<mat-card class="mb-3">
|
||||
<mat-card appearance="outlined" class="mb-3">
|
||||
<mat-card-content>
|
||||
<div class="container p-0">
|
||||
<div class="flex-nowrap no-gutters row">
|
||||
@ -126,7 +130,7 @@
|
||||
</div>
|
||||
</mat-card-content>
|
||||
</mat-card>
|
||||
<mat-card class="mb-3">
|
||||
<mat-card appearance="outlined" class="mb-3">
|
||||
<mat-card-content>
|
||||
<div class="container p-0">
|
||||
<div class="flex-nowrap no-gutters row">
|
||||
@ -152,7 +156,7 @@
|
||||
</div>
|
||||
</mat-card-content>
|
||||
</mat-card>
|
||||
<mat-card class="mb-3">
|
||||
<mat-card appearance="outlined" class="mb-3">
|
||||
<mat-card-content>
|
||||
<div class="container p-0">
|
||||
<div class="flex-nowrap no-gutters row">
|
||||
@ -178,7 +182,7 @@
|
||||
</div>
|
||||
</mat-card-content>
|
||||
</mat-card>
|
||||
<mat-card class="mb-3">
|
||||
<mat-card appearance="outlined" class="mb-3">
|
||||
<mat-card-content>
|
||||
<div class="container p-0">
|
||||
<div class="flex-nowrap no-gutters row">
|
||||
@ -204,7 +208,7 @@
|
||||
</div>
|
||||
</mat-card-content>
|
||||
</mat-card>
|
||||
<mat-card class="mb-3">
|
||||
<mat-card appearance="outlined" class="mb-3">
|
||||
<mat-card-content>
|
||||
<div class="container p-0">
|
||||
<div class="flex-nowrap no-gutters row">
|
||||
@ -228,7 +232,7 @@
|
||||
</div>
|
||||
</mat-card-content>
|
||||
</mat-card>
|
||||
<mat-card class="mb-3">
|
||||
<mat-card appearance="outlined" class="mb-3">
|
||||
<mat-card-content>
|
||||
<div class="container p-0">
|
||||
<div class="flex-nowrap no-gutters row">
|
||||
|
@ -1,6 +1,6 @@
|
||||
import { CommonModule } from '@angular/common';
|
||||
import { CUSTOM_ELEMENTS_SCHEMA, NgModule } from '@angular/core';
|
||||
import { MatLegacyCardModule as MatCardModule } from '@angular/material/legacy-card';
|
||||
import { MatCardModule } from '@angular/material/card';
|
||||
|
||||
import { BlogPageRoutingModule } from './blog-page-routing.module';
|
||||
import { BlogPageComponent } from './blog-page.component';
|
||||
|
@ -1,5 +1,7 @@
|
||||
import { Component, OnDestroy } from '@angular/core';
|
||||
import { Subject } from 'rxjs';
|
||||
import { ChangeDetectorRef, Component, OnDestroy } from '@angular/core';
|
||||
import { UserService } from '@ghostfolio/client/services/user/user.service';
|
||||
import { User } from '@ghostfolio/common/interfaces';
|
||||
import { Subject, takeUntil } from 'rxjs';
|
||||
|
||||
@Component({
|
||||
host: { class: 'page' },
|
||||
@ -8,9 +10,26 @@ import { Subject } from 'rxjs';
|
||||
templateUrl: './faq-page.html'
|
||||
})
|
||||
export class FaqPageComponent implements OnDestroy {
|
||||
public user: User;
|
||||
|
||||
private unsubscribeSubject = new Subject<void>();
|
||||
|
||||
public constructor() {}
|
||||
public constructor(
|
||||
private changeDetectorRef: ChangeDetectorRef,
|
||||
private userService: UserService
|
||||
) {}
|
||||
|
||||
public ngOnInit() {
|
||||
this.userService.stateChanged
|
||||
.pipe(takeUntil(this.unsubscribeSubject))
|
||||
.subscribe((state) => {
|
||||
if (state?.user) {
|
||||
this.user = state.user;
|
||||
|
||||
this.changeDetectorRef.markForCheck();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
public ngOnDestroy() {
|
||||
this.unsubscribeSubject.next();
|
||||
|
@ -115,7 +115,7 @@
|
||||
>.</mat-card-content
|
||||
>
|
||||
</mat-card>
|
||||
<mat-card class="mb-3">
|
||||
<mat-card *ngIf="user?.subscription?.type === 'Premium'" class="mb-3">
|
||||
<mat-card-title
|
||||
>I cannot find my broker in the list of platforms. What can I
|
||||
do?</mat-card-title
|
||||
|
@ -52,8 +52,11 @@ export class LandingPageComponent implements OnDestroy, OnInit {
|
||||
private dataService: DataService,
|
||||
private deviceService: DeviceDetectorService
|
||||
) {
|
||||
const { countriesOfSubscribers, globalPermissions, statistics } =
|
||||
this.dataService.fetchInfo();
|
||||
const {
|
||||
countriesOfSubscribers = [],
|
||||
globalPermissions,
|
||||
statistics
|
||||
} = this.dataService.fetchInfo();
|
||||
|
||||
for (const country of countriesOfSubscribers) {
|
||||
this.countriesOfSubscribersMap[country] = {
|
||||
|
@ -52,6 +52,7 @@
|
||||
|
||||
<div *ngIf="hasPermissionForStatistics" class="row mb-5">
|
||||
<div
|
||||
*ngIf="hasPermissionForSubscription"
|
||||
class="col-md-4 d-flex my-1"
|
||||
[ngClass]="{ 'justify-content-center': this.deviceType !== 'mobile' }"
|
||||
>
|
||||
@ -68,6 +69,24 @@
|
||||
>
|
||||
</a>
|
||||
</div>
|
||||
<div
|
||||
*ngIf="!hasPermissionForSubscription"
|
||||
class="col-md-4 d-flex my-1"
|
||||
[ngClass]="{ 'justify-content-center': this.deviceType !== 'mobile' }"
|
||||
>
|
||||
<a
|
||||
class="d-block"
|
||||
title="Ghostfolio in Numbers: Contributors on GitHub"
|
||||
[routerLink]="['/about']"
|
||||
>
|
||||
<gf-value
|
||||
icon="people-outline"
|
||||
size="large"
|
||||
[value]="statistics?.gitHubContributors ?? '-'"
|
||||
>Contributors on GitHub</gf-value
|
||||
>
|
||||
</a>
|
||||
</div>
|
||||
<div
|
||||
class="col-md-4 d-flex my-1"
|
||||
[ngClass]="{ 'justify-content-center': this.deviceType !== 'mobile' }"
|
||||
@ -300,7 +319,7 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row my-3">
|
||||
<div *ngIf="hasPermissionForSubscription" class="row my-3">
|
||||
<div class="col-12">
|
||||
<h2 class="h4 mb-1 text-center">
|
||||
How does <strong>Ghostfolio</strong> work?
|
||||
@ -357,15 +376,6 @@
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="downloads my-5 row justify-content-center">
|
||||
<a
|
||||
href="https://play.google.com/store/apps/details?id=ch.dotsilver.ghostfolio.twa"
|
||||
title="Get Ghostfolio on Google Play"
|
||||
>
|
||||
<img alt="Google Play Badge" src="../assets/badge-en-google-play.png" />
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="container">
|
||||
|
@ -13,12 +13,6 @@
|
||||
aspect-ratio: 16 / 9;
|
||||
}
|
||||
|
||||
.downloads {
|
||||
img {
|
||||
height: 2.5rem;
|
||||
}
|
||||
}
|
||||
|
||||
.intro {
|
||||
font-size: 4vw;
|
||||
line-height: 1;
|
||||
|
@ -36,7 +36,6 @@ export class ActivitiesPageComponent implements OnDestroy, OnInit {
|
||||
public hasImpersonationId: boolean;
|
||||
public hasPermissionToCreateActivity: boolean;
|
||||
public hasPermissionToDeleteActivity: boolean;
|
||||
public hasPermissionToImportActivities: boolean;
|
||||
public routeQueryParams: Subscription;
|
||||
public user: User;
|
||||
|
||||
@ -91,10 +90,6 @@ export class ActivitiesPageComponent implements OnDestroy, OnInit {
|
||||
.pipe(takeUntil(this.unsubscribeSubject))
|
||||
.subscribe((aId) => {
|
||||
this.hasImpersonationId = !!aId;
|
||||
|
||||
this.hasPermissionToImportActivities =
|
||||
hasPermission(globalPermissions, permissions.enableImport) &&
|
||||
!this.hasImpersonationId;
|
||||
});
|
||||
|
||||
this.userService.stateChanged
|
||||
@ -356,13 +351,11 @@ export class ActivitiesPageComponent implements OnDestroy, OnInit {
|
||||
return account.isDefault;
|
||||
})?.id;
|
||||
|
||||
this.hasPermissionToCreateActivity = hasPermission(
|
||||
this.user.permissions,
|
||||
permissions.createOrder
|
||||
);
|
||||
this.hasPermissionToDeleteActivity = hasPermission(
|
||||
this.user.permissions,
|
||||
permissions.deleteOrder
|
||||
);
|
||||
this.hasPermissionToCreateActivity =
|
||||
!this.hasImpersonationId &&
|
||||
hasPermission(this.user.permissions, permissions.createOrder);
|
||||
this.hasPermissionToDeleteActivity =
|
||||
!this.hasImpersonationId &&
|
||||
hasPermission(this.user.permissions, permissions.deleteOrder);
|
||||
}
|
||||
}
|
||||
|
@ -8,7 +8,6 @@
|
||||
[deviceType]="deviceType"
|
||||
[hasPermissionToCreateActivity]="hasPermissionToCreateActivity"
|
||||
[hasPermissionToExportActivities]="!hasImpersonationId"
|
||||
[hasPermissionToImportActivities]="hasPermissionToImportActivities"
|
||||
[locale]="user?.settings?.locale"
|
||||
[showActions]="!hasImpersonationId && hasPermissionToDeleteActivity && !user.settings.isRestrictedView"
|
||||
(activityDeleted)="onDeleteActivity($event)"
|
||||
@ -33,7 +32,7 @@
|
||||
[queryParams]="{ createDialog: true }"
|
||||
[routerLink]="[]"
|
||||
>
|
||||
<ion-icon name="add-outline" size="large"></ion-icon>
|
||||
<ion-icon class="mt-2" name="add-outline" size="large"></ion-icon>
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
|
@ -1,7 +1,7 @@
|
||||
import { CommonModule } from '@angular/common';
|
||||
import { CUSTOM_ELEMENTS_SCHEMA, NgModule } from '@angular/core';
|
||||
import { MatLegacyButtonModule as MatButtonModule } from '@angular/material/legacy-button';
|
||||
import { MatLegacySnackBarModule as MatSnackBarModule } from '@angular/material/legacy-snack-bar';
|
||||
import { MatButtonModule } from '@angular/material/button';
|
||||
import { MatSnackBarModule } from '@angular/material/snack-bar';
|
||||
import { RouterModule } from '@angular/router';
|
||||
import { ImportActivitiesService } from '@ghostfolio/client/services/import-activities.service';
|
||||
import { GfActivitiesTableModule } from '@ghostfolio/ui/activities-table/activities-table.module';
|
||||
|
@ -10,7 +10,7 @@ import {
|
||||
} from '@angular/core';
|
||||
import { FormBuilder, FormGroup, Validators } from '@angular/forms';
|
||||
import { DateAdapter, MAT_DATE_LOCALE } from '@angular/material/core';
|
||||
import { MatLegacyAutocompleteSelectedEvent as MatAutocompleteSelectedEvent } from '@angular/material/legacy-autocomplete';
|
||||
import { MatAutocompleteSelectedEvent } from '@angular/material/autocomplete';
|
||||
import {
|
||||
MAT_LEGACY_DIALOG_DATA as MAT_DIALOG_DATA,
|
||||
MatLegacyDialogRef as MatDialogRef
|
||||
|
@ -6,7 +6,7 @@
|
||||
>
|
||||
<h1 *ngIf="data.activity.id" i18n mat-dialog-title>Update activity</h1>
|
||||
<h1 *ngIf="!data.activity.id" i18n mat-dialog-title>Add activity</h1>
|
||||
<div class="flex-grow-1" mat-dialog-content>
|
||||
<div class="flex-grow-1 pt-3" mat-dialog-content>
|
||||
<div>
|
||||
<mat-form-field appearance="outline" class="w-100">
|
||||
<mat-label i18n>Type</mat-label>
|
||||
@ -76,7 +76,7 @@
|
||||
<div class="d-none">
|
||||
<mat-form-field appearance="outline" class="w-100">
|
||||
<mat-label i18n>Currency</mat-label>
|
||||
<mat-select class="no-arrow" formControlName="currency">
|
||||
<mat-select formControlName="currency">
|
||||
<mat-option *ngFor="let currency of currencies" [value]="currency"
|
||||
>{{ currency }}</mat-option
|
||||
>
|
||||
@ -93,7 +93,7 @@
|
||||
<mat-form-field appearance="outline" class="w-100">
|
||||
<mat-label i18n>Date</mat-label>
|
||||
<input formControlName="date" matInput [matDatepicker]="date" />
|
||||
<mat-datepicker-toggle matSuffix [for]="date">
|
||||
<mat-datepicker-toggle class="mr-2" matSuffix [for]="date">
|
||||
<ion-icon
|
||||
class="text-muted"
|
||||
matDatepickerToggleIcon
|
||||
@ -109,7 +109,7 @@
|
||||
<input formControlName="quantity" matInput type="number" />
|
||||
</mat-form-field>
|
||||
</div>
|
||||
<div>
|
||||
<div class="align-items-start d-flex">
|
||||
<mat-form-field appearance="outline" class="w-100">
|
||||
<mat-label
|
||||
><ng-container [ngSwitch]="activityForm.controls['type']?.value">
|
||||
@ -121,20 +121,20 @@
|
||||
</ng-container>
|
||||
</mat-label>
|
||||
<input formControlName="unitPrice" matInput type="number" />
|
||||
<span class="ml-2" matSuffix
|
||||
<span class="ml-2" matTextSuffix
|
||||
>{{ activityForm.controls['currency'].value }}</span
|
||||
>
|
||||
<button
|
||||
*ngIf="currentMarketPrice && (data.activity.type === 'BUY' || data.activity.type === 'SELL')"
|
||||
mat-icon-button
|
||||
matSuffix
|
||||
title="Apply current market price"
|
||||
type="button"
|
||||
(click)="applyCurrentMarketPrice()"
|
||||
>
|
||||
<ion-icon class="text-muted" name="refresh-outline"></ion-icon>
|
||||
</button>
|
||||
</mat-form-field>
|
||||
<button
|
||||
*ngIf="currentMarketPrice && (data.activity.type === 'BUY' || data.activity.type === 'SELL')"
|
||||
class="apply-current-market-price ml-2 no-min-width"
|
||||
mat-button
|
||||
title="Apply current market price"
|
||||
type="button"
|
||||
(click)="applyCurrentMarketPrice()"
|
||||
>
|
||||
<ion-icon class="text-muted" name="refresh-outline"></ion-icon>
|
||||
</button>
|
||||
</div>
|
||||
<div>
|
||||
<mat-form-field appearance="outline" class="w-100">
|
||||
@ -142,7 +142,7 @@
|
||||
<input formControlName="feeInCustomCurrency" matInput type="number" />
|
||||
<div
|
||||
class="ml-2"
|
||||
matSuffix
|
||||
matTextSuffix
|
||||
[ngClass]="{ 'd-none': !activityForm.controls['currency']?.value }"
|
||||
>
|
||||
<mat-select formControlName="currencyOfFee">
|
||||
@ -157,7 +157,7 @@
|
||||
<mat-form-field appearance="outline" class="w-100">
|
||||
<mat-label i18n>Fee</mat-label>
|
||||
<input formControlName="fee" matInput type="number" />
|
||||
<span class="ml-2" matSuffix
|
||||
<span class="ml-2" matTextSuffix
|
||||
>{{ activityForm.controls['currency'].value }}</span
|
||||
>
|
||||
</mat-form-field>
|
||||
@ -207,8 +207,8 @@
|
||||
<div [ngClass]="{ 'd-none': tags?.length <= 0 }">
|
||||
<mat-form-field appearance="outline" class="w-100">
|
||||
<mat-label i18n>Tags</mat-label>
|
||||
<mat-chip-list #tagsChipList>
|
||||
<mat-chip
|
||||
<mat-chip-grid #tagsChipList>
|
||||
<mat-chip-row
|
||||
*ngFor="let tag of activityForm.controls['tags']?.value"
|
||||
matChipRemove
|
||||
[removable]="true"
|
||||
@ -216,7 +216,7 @@
|
||||
>
|
||||
{{ tag.name }}
|
||||
<ion-icon class="ml-2" matPrefix name="close-outline"></ion-icon>
|
||||
</mat-chip>
|
||||
</mat-chip-row>
|
||||
<input
|
||||
#tagInput
|
||||
name="close-outline"
|
||||
@ -224,7 +224,7 @@
|
||||
[matChipInputFor]="tagsChipList"
|
||||
[matChipInputSeparatorKeyCodes]="separatorKeysCodes"
|
||||
/>
|
||||
</mat-chip-list>
|
||||
</mat-chip-grid>
|
||||
<mat-autocomplete
|
||||
#autocompleteTags="matAutocomplete"
|
||||
(optionSelected)="onAddTag($event)"
|
||||
|
@ -2,14 +2,14 @@ import { CommonModule } from '@angular/common';
|
||||
import { CUSTOM_ELEMENTS_SCHEMA, NgModule } from '@angular/core';
|
||||
import { FormsModule, ReactiveFormsModule } from '@angular/forms';
|
||||
import { MatDatepickerModule } from '@angular/material/datepicker';
|
||||
import { MatLegacyAutocompleteModule as MatAutocompleteModule } from '@angular/material/legacy-autocomplete';
|
||||
import { MatLegacyButtonModule as MatButtonModule } from '@angular/material/legacy-button';
|
||||
import { MatLegacyChipsModule as MatChipsModule } from '@angular/material/legacy-chips';
|
||||
import { MatAutocompleteModule } from '@angular/material/autocomplete';
|
||||
import { MatButtonModule } from '@angular/material/button';
|
||||
import { MatChipsModule } from '@angular/material/chips';
|
||||
import { MatLegacyDialogModule as MatDialogModule } from '@angular/material/legacy-dialog';
|
||||
import { MatLegacyFormFieldModule as MatFormFieldModule } from '@angular/material/legacy-form-field';
|
||||
import { MatLegacyInputModule as MatInputModule } from '@angular/material/legacy-input';
|
||||
import { MatLegacyProgressSpinnerModule as MatProgressSpinnerModule } from '@angular/material/legacy-progress-spinner';
|
||||
import { MatLegacySelectModule as MatSelectModule } from '@angular/material/legacy-select';
|
||||
import { MatFormFieldModule } from '@angular/material/form-field';
|
||||
import { MatInputModule } from '@angular/material/input';
|
||||
import { MatProgressSpinnerModule } from '@angular/material/progress-spinner';
|
||||
import { MatSelectModule } from '@angular/material/select';
|
||||
import { GfSymbolModule } from '@ghostfolio/client/pipes/symbol/symbol.module';
|
||||
import { GfValueModule } from '@ghostfolio/ui/value';
|
||||
|
||||
|
@ -20,45 +20,24 @@
|
||||
}
|
||||
}
|
||||
|
||||
.mat-chip {
|
||||
cursor: pointer;
|
||||
min-height: 1.5rem !important;
|
||||
}
|
||||
|
||||
.mat-form-field-appearance-outline {
|
||||
::ng-deep {
|
||||
.mat-form-field-suffix {
|
||||
top: -0.3rem;
|
||||
}
|
||||
}
|
||||
|
||||
ion-icon {
|
||||
font-size: 130%;
|
||||
}
|
||||
}
|
||||
|
||||
.mat-select {
|
||||
&.no-arrow {
|
||||
::ng-deep {
|
||||
.mat-select-arrow {
|
||||
opacity: 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.mat-datepicker-input {
|
||||
&.mat-input-element:disabled {
|
||||
&.mat-mdc-input-element:disabled {
|
||||
color: var(--dark-primary-text);
|
||||
}
|
||||
}
|
||||
|
||||
.mat-mdc-button {
|
||||
&.apply-current-market-price {
|
||||
height: 56px;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
:host-context(.is-dark-theme) {
|
||||
.mat-dialog-content {
|
||||
.mat-datepicker-input {
|
||||
&.mat-input-element:disabled {
|
||||
&.mat-mdc-input-element:disabled {
|
||||
color: var(--light-primary-text);
|
||||
}
|
||||
}
|
||||
|
@ -11,6 +11,7 @@ import {
|
||||
MatLegacyDialogRef as MatDialogRef
|
||||
} from '@angular/material/legacy-dialog';
|
||||
import { MatLegacySnackBar as MatSnackBar } from '@angular/material/legacy-snack-bar';
|
||||
import { CreateAccountDto } from '@ghostfolio/api/app/account/create-account.dto';
|
||||
import { Activity } from '@ghostfolio/api/app/order/interfaces/activities.interface';
|
||||
import { DataService } from '@ghostfolio/client/services/data.service';
|
||||
import { ImportActivitiesService } from '@ghostfolio/client/services/import-activities.service';
|
||||
@ -28,6 +29,7 @@ import { ImportActivitiesDialogParams } from './interfaces/interfaces';
|
||||
templateUrl: 'import-activities-dialog.html'
|
||||
})
|
||||
export class ImportActivitiesDialog implements OnDestroy {
|
||||
public accounts: CreateAccountDto[] = [];
|
||||
public activities: Activity[] = [];
|
||||
public details: any[] = [];
|
||||
public errorMessages: string[] = [];
|
||||
@ -91,9 +93,10 @@ export class ImportActivitiesDialog implements OnDestroy {
|
||||
try {
|
||||
this.snackBar.open('⏳ ' + $localize`Importing data...`);
|
||||
|
||||
await this.importActivitiesService.importSelectedActivities(
|
||||
this.selectedActivities
|
||||
);
|
||||
await this.importActivitiesService.importSelectedActivities({
|
||||
accounts: this.accounts,
|
||||
activities: this.selectedActivities
|
||||
});
|
||||
|
||||
this.snackBar.open(
|
||||
'✅ ' + $localize`Import has been completed`,
|
||||
@ -163,6 +166,8 @@ export class ImportActivitiesDialog implements OnDestroy {
|
||||
if (file.name.endsWith('.json')) {
|
||||
const content = JSON.parse(fileContent);
|
||||
|
||||
this.accounts = content.accounts;
|
||||
|
||||
if (!isArray(content.activities)) {
|
||||
if (isArray(content.orders)) {
|
||||
this.handleImportError({
|
||||
@ -180,10 +185,13 @@ export class ImportActivitiesDialog implements OnDestroy {
|
||||
}
|
||||
|
||||
try {
|
||||
this.activities = await this.importActivitiesService.importJson({
|
||||
content: content.activities,
|
||||
isDryRun: true
|
||||
});
|
||||
const { activities } =
|
||||
await this.importActivitiesService.importJson({
|
||||
accounts: content.accounts,
|
||||
activities: content.activities,
|
||||
isDryRun: true
|
||||
});
|
||||
this.activities = activities;
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
this.handleImportError({ error, activities: content.activities });
|
||||
@ -192,11 +200,12 @@ export class ImportActivitiesDialog implements OnDestroy {
|
||||
return;
|
||||
} else if (file.name.endsWith('.csv')) {
|
||||
try {
|
||||
this.activities = await this.importActivitiesService.importCsv({
|
||||
const data = await this.importActivitiesService.importCsv({
|
||||
fileContent,
|
||||
isDryRun: true,
|
||||
userAccounts: this.data.user.accounts
|
||||
});
|
||||
this.activities = data.activities;
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
this.handleImportError({
|
||||
|
@ -70,7 +70,6 @@
|
||||
[hasPermissionToCreateActivity]="false"
|
||||
[hasPermissionToExportActivities]="false"
|
||||
[hasPermissionToFilter]="false"
|
||||
[hasPermissionToImportActivities]="false"
|
||||
[hasPermissionToOpenDetails]="false"
|
||||
[locale]="data?.user?.settings?.locale"
|
||||
[showActions]="false"
|
||||
|
@ -2,6 +2,7 @@ import { ChangeDetectorRef, Component, OnDestroy, OnInit } from '@angular/core';
|
||||
import { DataService } from '@ghostfolio/client/services/data.service';
|
||||
import { UserService } from '@ghostfolio/client/services/user/user.service';
|
||||
import { User } from '@ghostfolio/common/interfaces';
|
||||
import { translate } from '@ghostfolio/ui/i18n';
|
||||
import { Subject } from 'rxjs';
|
||||
import { takeUntil } from 'rxjs/operators';
|
||||
|
||||
@ -14,6 +15,12 @@ import { takeUntil } from 'rxjs/operators';
|
||||
export class PricingPageComponent implements OnDestroy, OnInit {
|
||||
public baseCurrency: string;
|
||||
public coupon: number;
|
||||
public importAndExportTooltipOSS = translate(
|
||||
'DATA_IMPORT_AND_EXPORT_TOOLTIP_OSS'
|
||||
);
|
||||
public importAndExportTooltipPremium = translate(
|
||||
'DATA_IMPORT_AND_EXPORT_TOOLTIP_PREMIUM'
|
||||
);
|
||||
public isLoggedIn: boolean;
|
||||
public price: number;
|
||||
public user: User;
|
||||
|
@ -85,6 +85,20 @@
|
||||
></ion-icon>
|
||||
<span i18n>FIRE Calculator</span>
|
||||
</li>
|
||||
<li class="align-items-center d-flex mb-1">
|
||||
<ion-icon
|
||||
class="mr-1"
|
||||
name="checkmark-circle-outline"
|
||||
></ion-icon>
|
||||
<span i18n>Data Import and Export</span>
|
||||
<span
|
||||
class="align-items-center d-flex ml-1"
|
||||
matTooltipPosition="above"
|
||||
[matTooltip]="importAndExportTooltipOSS"
|
||||
>
|
||||
<ion-icon name="information-circle-outline"></ion-icon>
|
||||
</span>
|
||||
</li>
|
||||
<li class="align-items-center d-flex mb-1">
|
||||
<ion-icon
|
||||
class="mr-1"
|
||||
@ -261,6 +275,20 @@
|
||||
></ion-icon>
|
||||
<span i18n>FIRE Calculator</span>
|
||||
</li>
|
||||
<li class="align-items-center d-flex mb-1">
|
||||
<ion-icon
|
||||
class="mr-1"
|
||||
name="checkmark-circle-outline"
|
||||
></ion-icon>
|
||||
<span i18n>Data Import and Export</span>
|
||||
<span
|
||||
class="align-items-center d-flex ml-1"
|
||||
matTooltipPosition="above"
|
||||
[matTooltip]="importAndExportTooltipPremium"
|
||||
>
|
||||
<ion-icon name="information-circle-outline"></ion-icon>
|
||||
</span>
|
||||
</li>
|
||||
<li class="align-items-center d-flex mb-1">
|
||||
<ion-icon
|
||||
class="mr-1"
|
||||
|
@ -2,6 +2,7 @@ import { CommonModule } from '@angular/common';
|
||||
import { CUSTOM_ELEMENTS_SCHEMA, NgModule } from '@angular/core';
|
||||
import { MatLegacyButtonModule as MatButtonModule } from '@angular/material/legacy-button';
|
||||
import { MatLegacyCardModule as MatCardModule } from '@angular/material/legacy-card';
|
||||
import { MatLegacyTooltipModule as MatTooltipModule } from '@angular/material/legacy-tooltip';
|
||||
import { RouterModule } from '@angular/router';
|
||||
import { GfPremiumIndicatorModule } from '@ghostfolio/ui/premium-indicator';
|
||||
|
||||
@ -15,6 +16,7 @@ import { PricingPageComponent } from './pricing-page.component';
|
||||
GfPremiumIndicatorModule,
|
||||
MatButtonModule,
|
||||
MatCardModule,
|
||||
MatTooltipModule,
|
||||
PricingPageRoutingModule,
|
||||
RouterModule
|
||||
],
|
||||
|
@ -4,6 +4,7 @@ import { Router } from '@angular/router';
|
||||
import { DataService } from '@ghostfolio/client/services/data.service';
|
||||
import { InternetIdentityService } from '@ghostfolio/client/services/internet-identity.service';
|
||||
import { TokenStorageService } from '@ghostfolio/client/services/token-storage.service';
|
||||
import { UserService } from '@ghostfolio/client/services/user/user.service';
|
||||
import { InfoItem, LineChartItem } from '@ghostfolio/common/interfaces';
|
||||
import { hasPermission, permissions } from '@ghostfolio/common/permissions';
|
||||
import { Role } from '@prisma/client';
|
||||
@ -37,7 +38,8 @@ export class RegisterPageComponent implements OnDestroy, OnInit {
|
||||
private dialog: MatDialog,
|
||||
private internetIdentityService: InternetIdentityService,
|
||||
private router: Router,
|
||||
private tokenStorageService: TokenStorageService
|
||||
private tokenStorageService: TokenStorageService,
|
||||
private userService: UserService
|
||||
) {
|
||||
this.info = this.dataService.fetchInfo();
|
||||
|
||||
@ -61,7 +63,7 @@ export class RegisterPageComponent implements OnDestroy, OnInit {
|
||||
|
||||
public async createAccount() {
|
||||
this.dataService
|
||||
.postUser()
|
||||
.postUser({ country: this.userService.getCountry() })
|
||||
.pipe(takeUntil(this.unsubscribeSubject))
|
||||
.subscribe(({ accessToken, authToken, role }) => {
|
||||
this.openShowAccessTokenDialog(accessToken, authToken, role);
|
||||
|
@ -1,7 +1,7 @@
|
||||
<div class="container">
|
||||
<div class="row">
|
||||
<div class="col">
|
||||
<h1 class="d-none d-sm-block mb-3 text-center" i18n>Resources</h1>
|
||||
<h1 class="d-none d-sm-block h3 mb-3 text-center" i18n>Resources</h1>
|
||||
<h2 class="h4 mb-3">Guides</h2>
|
||||
<div class="mb-5">
|
||||
<div class="mb-4 media">
|
||||
|
@ -1,13 +1,12 @@
|
||||
import { CommonModule } from '@angular/common';
|
||||
import { CUSTOM_ELEMENTS_SCHEMA, NgModule } from '@angular/core';
|
||||
import { MatLegacyCardModule as MatCardModule } from '@angular/material/legacy-card';
|
||||
|
||||
import { ResourcesPageRoutingModule } from './resources-page-routing.module';
|
||||
import { ResourcesPageComponent } from './resources-page.component';
|
||||
|
||||
@NgModule({
|
||||
declarations: [ResourcesPageComponent],
|
||||
imports: [CommonModule, MatCardModule, ResourcesPageRoutingModule],
|
||||
imports: [CommonModule, ResourcesPageRoutingModule],
|
||||
schemas: [CUSTOM_ELEMENTS_SCHEMA]
|
||||
})
|
||||
export class ResourcesPageModule {}
|
||||
|
@ -405,8 +405,8 @@ export class DataService {
|
||||
return this.http.post<OrderModel>(`/api/v1/order`, aOrder);
|
||||
}
|
||||
|
||||
public postUser() {
|
||||
return this.http.post<UserItem>(`/api/v1/user`, {});
|
||||
public postUser({ country }: { country: string }) {
|
||||
return this.http.post<UserItem>(`/api/v1/user`, { country });
|
||||
}
|
||||
|
||||
public putAccount(aAccount: UpdateAccountDto) {
|
||||
|
@ -1,5 +1,6 @@
|
||||
import { HttpClient } from '@angular/common/http';
|
||||
import { Injectable } from '@angular/core';
|
||||
import { CreateAccountDto } from '@ghostfolio/api/app/account/create-account.dto';
|
||||
import { CreateOrderDto } from '@ghostfolio/api/app/order/create-order.dto';
|
||||
import { Activity } from '@ghostfolio/api/app/order/interfaces/activities.interface';
|
||||
import { Account, DataSource, Type } from '@prisma/client';
|
||||
@ -33,7 +34,9 @@ export class ImportActivitiesService {
|
||||
fileContent: string;
|
||||
isDryRun?: boolean;
|
||||
userAccounts: Account[];
|
||||
}): Promise<Activity[]> {
|
||||
}): Promise<{
|
||||
activities: Activity[];
|
||||
}> {
|
||||
const content = csvToJson(fileContent, {
|
||||
dynamicTyping: true,
|
||||
header: true,
|
||||
@ -55,20 +58,26 @@ export class ImportActivitiesService {
|
||||
});
|
||||
}
|
||||
|
||||
return await this.importJson({ isDryRun, content: activities });
|
||||
return await this.importJson({ activities, isDryRun });
|
||||
}
|
||||
|
||||
public importJson({
|
||||
content,
|
||||
accounts,
|
||||
activities,
|
||||
isDryRun = false
|
||||
}: {
|
||||
content: CreateOrderDto[];
|
||||
activities: CreateOrderDto[];
|
||||
accounts?: CreateAccountDto[];
|
||||
isDryRun?: boolean;
|
||||
}): Promise<Activity[]> {
|
||||
}): Promise<{
|
||||
activities: Activity[];
|
||||
accounts?: CreateAccountDto[];
|
||||
}> {
|
||||
return new Promise((resolve, reject) => {
|
||||
this.postImport(
|
||||
{
|
||||
activities: content
|
||||
accounts,
|
||||
activities
|
||||
},
|
||||
isDryRun
|
||||
)
|
||||
@ -80,22 +89,29 @@ export class ImportActivitiesService {
|
||||
)
|
||||
.subscribe({
|
||||
next: (data) => {
|
||||
resolve(data.activities);
|
||||
resolve(data);
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
public importSelectedActivities(
|
||||
selectedActivities: Activity[]
|
||||
): Promise<Activity[]> {
|
||||
public importSelectedActivities({
|
||||
accounts,
|
||||
activities
|
||||
}: {
|
||||
accounts: CreateAccountDto[];
|
||||
activities: Activity[];
|
||||
}): Promise<{
|
||||
activities: Activity[];
|
||||
accounts?: CreateAccountDto[];
|
||||
}> {
|
||||
const importData: CreateOrderDto[] = [];
|
||||
|
||||
for (const activity of selectedActivities) {
|
||||
for (const activity of activities) {
|
||||
importData.push(this.convertToCreateOrderDto(activity));
|
||||
}
|
||||
|
||||
return this.importJson({ content: importData });
|
||||
return this.importJson({ accounts, activities: importData });
|
||||
}
|
||||
|
||||
private convertToCreateOrderDto({
|
||||
@ -347,7 +363,7 @@ export class ImportActivitiesService {
|
||||
}
|
||||
|
||||
private postImport(
|
||||
aImportData: { activities: CreateOrderDto[] },
|
||||
aImportData: { accounts: CreateAccountDto[]; activities: CreateOrderDto[] },
|
||||
aIsDryRun = false
|
||||
) {
|
||||
return this.http.post<{ activities: Activity[] }>(
|
||||
|
@ -6,6 +6,7 @@ import { SubscriptionInterstitialDialogParams } from '@ghostfolio/client/compone
|
||||
import { SubscriptionInterstitialDialog } from '@ghostfolio/client/components/subscription-interstitial-dialog/subscription-interstitial-dialog.component';
|
||||
import { User } from '@ghostfolio/common/interfaces';
|
||||
import { hasPermission, permissions } from '@ghostfolio/common/permissions';
|
||||
import { timezoneCitiesToCountries } from '@ghostfolio/common/timezone-cities-to-countries';
|
||||
import { DeviceDetectorService } from 'ngx-device-detector';
|
||||
import { Subject, of } from 'rxjs';
|
||||
import { throwError } from 'rxjs';
|
||||
@ -45,6 +46,20 @@ export class UserService extends ObservableStore<UserStoreState> {
|
||||
}
|
||||
}
|
||||
|
||||
public getCountry() {
|
||||
let country: string;
|
||||
|
||||
if (Intl) {
|
||||
const timeZone = Intl.DateTimeFormat().resolvedOptions().timeZone;
|
||||
const timeZoneArray = timeZone.split('/');
|
||||
const city = timeZoneArray[timeZoneArray.length - 1];
|
||||
|
||||
country = timezoneCitiesToCountries[city];
|
||||
}
|
||||
|
||||
return country;
|
||||
}
|
||||
|
||||
public remove() {
|
||||
this.setState({ user: null }, UserStoreActions.RemoveUser);
|
||||
}
|
||||
|
Binary file not shown.
Before Width: | Height: | Size: 13 KiB |
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
@ -203,7 +203,7 @@ body {
|
||||
|
||||
.blog {
|
||||
a {
|
||||
&:not(.mat-flat-button) {
|
||||
&:not(.mdc-button) {
|
||||
color: rgba(var(--palette-primary-500), 1) !important;
|
||||
font-weight: 500;
|
||||
|
||||
|
@ -118,6 +118,18 @@ export function getDateWithTimeFormatString(aLocale?: string) {
|
||||
return `${getDateFormatString(aLocale)}, HH:mm:ss`;
|
||||
}
|
||||
|
||||
export function getEmojiFlag(aCountryCode: string) {
|
||||
if (!aCountryCode) {
|
||||
return aCountryCode;
|
||||
}
|
||||
|
||||
return aCountryCode
|
||||
.toUpperCase()
|
||||
.replace(/./g, (character) =>
|
||||
String.fromCodePoint(127397 + character.charCodeAt(0))
|
||||
);
|
||||
}
|
||||
|
||||
export function getLocale() {
|
||||
return navigator.languages?.length
|
||||
? navigator.languages[0]
|
||||
|
@ -5,6 +5,7 @@ export interface AdminData {
|
||||
userCount: number;
|
||||
users: {
|
||||
accountCount: number;
|
||||
country: string;
|
||||
createdAt: Date;
|
||||
engagement: number;
|
||||
id: string;
|
||||
|
@ -1,10 +1,11 @@
|
||||
import { Order } from '@prisma/client';
|
||||
import { Account, Order } from '@prisma/client';
|
||||
|
||||
export interface Export {
|
||||
meta: {
|
||||
date: string;
|
||||
version: string;
|
||||
};
|
||||
accounts: Omit<Account, 'createdAt' | 'isDefault' | 'updatedAt' | 'userId'>[];
|
||||
activities: (Omit<
|
||||
Order,
|
||||
| 'accountUserId'
|
||||
|
426
libs/common/src/lib/timezone-cities-to-countries.ts
Normal file
426
libs/common/src/lib/timezone-cities-to-countries.ts
Normal file
@ -0,0 +1,426 @@
|
||||
export const timezoneCitiesToCountries = {
|
||||
Abidjan: 'CI',
|
||||
Accra: 'GH',
|
||||
Adak: 'US',
|
||||
Addis_Ababa: 'ET',
|
||||
Adelaide: 'AU',
|
||||
Aden: 'YE',
|
||||
Algiers: 'DZ',
|
||||
Almaty: 'KZ',
|
||||
Amman: 'JO',
|
||||
Amsterdam: 'NL',
|
||||
Anadyr: 'RU',
|
||||
Anchorage: 'US',
|
||||
Andorra: 'AD',
|
||||
Anguilla: 'AI',
|
||||
Antananarivo: 'MG',
|
||||
Antigua: 'AG',
|
||||
Apia: 'WS',
|
||||
Aqtau: 'KZ',
|
||||
Aqtobe: 'KZ',
|
||||
Araguaina: 'BR',
|
||||
Aruba: 'AW',
|
||||
Ashgabat: 'TM',
|
||||
Asmara: 'ER',
|
||||
Astrakhan: 'RU',
|
||||
Asuncion: 'PY',
|
||||
Athens: 'GR',
|
||||
Atikokan: 'CA',
|
||||
Atyrau: 'KZ',
|
||||
Auckland: 'NZ',
|
||||
Azores: 'PT',
|
||||
Baghdad: 'IQ',
|
||||
Bahia: 'BR',
|
||||
Bahia_Banderas: 'MX',
|
||||
Bahrain: 'BH',
|
||||
Baku: 'AZ',
|
||||
Bamako: 'ML',
|
||||
Bangkok: 'TH',
|
||||
Bangui: 'CF',
|
||||
Banjul: 'GM',
|
||||
Barbados: 'BB',
|
||||
Barnaul: 'RU',
|
||||
Beirut: 'LB',
|
||||
Belem: 'BR',
|
||||
Belgrade: 'RS',
|
||||
Belize: 'BZ',
|
||||
Berlin: 'DE',
|
||||
Bermuda: 'BM',
|
||||
Beulah: 'US',
|
||||
Bishkek: 'KG',
|
||||
Bissau: 'GW',
|
||||
'Blanc-Sablon': 'CA',
|
||||
Blantyre: 'MW',
|
||||
Boa_Vista: 'BR',
|
||||
Bogota: 'CO',
|
||||
Boise: 'US',
|
||||
Bougainville: 'PG',
|
||||
Bratislava: 'SK',
|
||||
Brazzaville: 'CG',
|
||||
Brisbane: 'AU',
|
||||
Broken_Hill: 'AU',
|
||||
Brunei: 'BN',
|
||||
Brussels: 'BE',
|
||||
Bucharest: 'RO',
|
||||
Budapest: 'HU',
|
||||
Buenos_Aires: 'AR',
|
||||
Bujumbura: 'BI',
|
||||
Busingen: 'DE',
|
||||
Cairo: 'EG',
|
||||
Cambridge_Bay: 'CA',
|
||||
Campo_Grande: 'BR',
|
||||
Canary: 'ES',
|
||||
Cancun: 'MX',
|
||||
Cape_Verde: 'CV',
|
||||
Caracas: 'VE',
|
||||
Casablanca: 'MA',
|
||||
Casey: 'AQ',
|
||||
Catamarca: 'AR',
|
||||
Cayenne: 'GF',
|
||||
Cayman: 'KY',
|
||||
Center: 'US',
|
||||
Ceuta: 'ES',
|
||||
Chagos: 'IO',
|
||||
Chatham: 'NZ',
|
||||
Chicago: 'US',
|
||||
Chihuahua: 'MX',
|
||||
Chisinau: 'MD',
|
||||
Chita: 'RU',
|
||||
Choibalsan: 'MN',
|
||||
Christmas: 'CX',
|
||||
Chuuk: 'FM',
|
||||
Cocos: 'CC',
|
||||
Colombo: 'LK',
|
||||
Comoro: 'KM',
|
||||
Conakry: 'GN',
|
||||
Copenhagen: 'DK',
|
||||
Cordoba: 'AR',
|
||||
Costa_Rica: 'CR',
|
||||
Creston: 'CA',
|
||||
Cuiaba: 'BR',
|
||||
Curacao: 'CW',
|
||||
Dakar: 'SN',
|
||||
Damascus: 'SY',
|
||||
Danmarkshavn: 'GL',
|
||||
Dar_es_Salaam: 'TZ',
|
||||
Darwin: 'AU',
|
||||
Davis: 'AQ',
|
||||
Dawson: 'CA',
|
||||
Dawson_Creek: 'CA',
|
||||
Denver: 'US',
|
||||
Detroit: 'US',
|
||||
Dhaka: 'BD',
|
||||
Dili: 'TL',
|
||||
Djibouti: 'DJ',
|
||||
Dominica: 'DM',
|
||||
Douala: 'CM',
|
||||
Dubai: 'AE',
|
||||
Dublin: 'IE',
|
||||
DumontDUrville: 'AQ',
|
||||
Dushanbe: 'TJ',
|
||||
Easter: 'CL',
|
||||
Edmonton: 'CA',
|
||||
Efate: 'VU',
|
||||
Eirunepe: 'BR',
|
||||
El_Aaiun: 'EH',
|
||||
El_Salvador: 'SV',
|
||||
Eucla: 'AU',
|
||||
Fakaofo: 'TK',
|
||||
Famagusta: 'CY',
|
||||
Faroe: 'FO',
|
||||
Fiji: 'FJ',
|
||||
Fort_Nelson: 'CA',
|
||||
Fortaleza: 'BR',
|
||||
Freetown: 'SL',
|
||||
Funafuti: 'TV',
|
||||
Gaborone: 'BW',
|
||||
Galapagos: 'EC',
|
||||
Gambier: 'PF',
|
||||
Gaza: 'PS',
|
||||
Gibraltar: 'GI',
|
||||
Glace_Bay: 'CA',
|
||||
Goose_Bay: 'CA',
|
||||
Grand_Turk: 'TC',
|
||||
Grenada: 'GD',
|
||||
Guadalcanal: 'SB',
|
||||
Guadeloupe: 'GP',
|
||||
Guam: 'GU',
|
||||
Guatemala: 'GT',
|
||||
Guayaquil: 'EC',
|
||||
Guernsey: 'GG',
|
||||
Guyana: 'GY',
|
||||
Halifax: 'CA',
|
||||
Harare: 'ZW',
|
||||
Havana: 'CU',
|
||||
Hebron: 'PS',
|
||||
Helsinki: 'FI',
|
||||
Hermosillo: 'MX',
|
||||
Ho_Chi_Minh: 'VN',
|
||||
Hobart: 'AU',
|
||||
Hong_Kong: 'HK',
|
||||
Honolulu: 'US',
|
||||
Hovd: 'MN',
|
||||
Indianapolis: 'US',
|
||||
Inuvik: 'CA',
|
||||
Iqaluit: 'CA',
|
||||
Irkutsk: 'RU',
|
||||
Isle_of_Man: 'IM',
|
||||
Istanbul: 'TR',
|
||||
Jakarta: 'ID',
|
||||
Jamaica: 'JM',
|
||||
Jayapura: 'ID',
|
||||
Jersey: 'JE',
|
||||
Jerusalem: 'IL',
|
||||
Johannesburg: 'ZA',
|
||||
Juba: 'SS',
|
||||
Jujuy: 'AR',
|
||||
Juneau: 'US',
|
||||
Kabul: 'AF',
|
||||
Kaliningrad: 'RU',
|
||||
Kamchatka: 'RU',
|
||||
Kampala: 'UG',
|
||||
Kanton: 'KI',
|
||||
Karachi: 'PK',
|
||||
Kathmandu: 'NP',
|
||||
Kerguelen: 'TF',
|
||||
Khandyga: 'RU',
|
||||
Khartoum: 'SD',
|
||||
Kiev: 'UA',
|
||||
Kigali: 'RW',
|
||||
Kinshasa: 'CD',
|
||||
Kiritimati: 'KI',
|
||||
Kirov: 'RU',
|
||||
Knox: 'US',
|
||||
Kolkata: 'IN',
|
||||
Kosrae: 'FM',
|
||||
Kralendijk: 'NL',
|
||||
Krasnoyarsk: 'RU',
|
||||
Kuala_Lumpur: 'MY',
|
||||
Kuching: 'MY',
|
||||
Kuwait: 'KW',
|
||||
Kwajalein: 'MH',
|
||||
La_Paz: 'BO',
|
||||
La_Rioja: 'AR',
|
||||
Lagos: 'NG',
|
||||
Libreville: 'GA',
|
||||
Lima: 'PE',
|
||||
Lindeman: 'AU',
|
||||
Lisbon: 'PT',
|
||||
Ljubljana: 'SI',
|
||||
Lome: 'TG',
|
||||
London: 'GB',
|
||||
Longyearbyen: 'SJ',
|
||||
Lord_Howe: 'AU',
|
||||
Los_Angeles: 'US',
|
||||
Louisville: 'US',
|
||||
Lower_Princes: 'SX',
|
||||
Luanda: 'AO',
|
||||
Lubumbashi: 'CD',
|
||||
Lusaka: 'ZM',
|
||||
Luxembourg: 'LU',
|
||||
Macau: 'MO',
|
||||
Maceio: 'BR',
|
||||
Macquarie: 'AU',
|
||||
Madeira: 'PT',
|
||||
Madrid: 'ES',
|
||||
Magadan: 'RU',
|
||||
Mahe: 'SC',
|
||||
Majuro: 'MH',
|
||||
Makassar: 'ID',
|
||||
Malabo: 'GQ',
|
||||
Maldives: 'MV',
|
||||
Malta: 'MT',
|
||||
Managua: 'NI',
|
||||
Manaus: 'BR',
|
||||
Manila: 'PH',
|
||||
Maputo: 'MZ',
|
||||
Marengo: 'US',
|
||||
Mariehamn: 'AX',
|
||||
Marigot: 'MF',
|
||||
Marquesas: 'PF',
|
||||
Martinique: 'MQ',
|
||||
Maseru: 'LS',
|
||||
Matamoros: 'MX',
|
||||
Mauritius: 'MU',
|
||||
Mawson: 'AQ',
|
||||
Mayotte: 'YT',
|
||||
Mazatlan: 'MX',
|
||||
Mbabane: 'SZ',
|
||||
McMurdo: 'AQ',
|
||||
Melbourne: 'AU',
|
||||
Mendoza: 'AR',
|
||||
Menominee: 'US',
|
||||
Merida: 'MX',
|
||||
Metlakatla: 'US',
|
||||
Mexico_City: 'MX',
|
||||
Midway: 'UM',
|
||||
Minsk: 'BY',
|
||||
Miquelon: 'PM',
|
||||
Mogadishu: 'SO',
|
||||
Monaco: 'MC',
|
||||
Moncton: 'CA',
|
||||
Monrovia: 'LR',
|
||||
Monterrey: 'MX',
|
||||
Montevideo: 'UY',
|
||||
Monticello: 'US',
|
||||
Montserrat: 'MS',
|
||||
Moscow: 'RU',
|
||||
Muscat: 'OM',
|
||||
Nairobi: 'KE',
|
||||
Nassau: 'BS',
|
||||
Nauru: 'NR',
|
||||
Ndjamena: 'TD',
|
||||
New_Salem: 'US',
|
||||
New_York: 'US',
|
||||
Niamey: 'NE',
|
||||
Nicosia: 'CY',
|
||||
Nipigon: 'CA',
|
||||
Niue: 'NU',
|
||||
Nome: 'US',
|
||||
Norfolk: 'NF',
|
||||
Noronha: 'BR',
|
||||
Nouakchott: 'MR',
|
||||
Noumea: 'NC',
|
||||
Novokuznetsk: 'RU',
|
||||
Novosibirsk: 'RU',
|
||||
Nuuk: 'GL',
|
||||
Ojinaga: 'MX',
|
||||
Omsk: 'RU',
|
||||
Oral: 'KZ',
|
||||
Oslo: 'NO',
|
||||
Ouagadougou: 'BF',
|
||||
Pago_Pago: 'AS',
|
||||
Palau: 'PW',
|
||||
Palmer: 'AQ',
|
||||
Panama: 'PA',
|
||||
Pangnirtung: 'CA',
|
||||
Paramaribo: 'SR',
|
||||
Paris: 'FR',
|
||||
Perth: 'AU',
|
||||
Petersburg: 'US',
|
||||
Phnom_Penh: 'KH',
|
||||
Phoenix: 'US',
|
||||
Pitcairn: 'PN',
|
||||
Podgorica: 'ME',
|
||||
Pohnpei: 'FM',
|
||||
Pontianak: 'ID',
|
||||
'Port-au-Prince': 'HT',
|
||||
Port_Moresby: 'PG',
|
||||
Port_of_Spain: 'TT',
|
||||
'Porto-Novo': 'BJ',
|
||||
Porto_Velho: 'BR',
|
||||
Prague: 'CZ',
|
||||
Puerto_Rico: 'PR',
|
||||
Punta_Arenas: 'CL',
|
||||
Pyongyang: 'KP',
|
||||
Qatar: 'QA',
|
||||
Qostanay: 'KZ',
|
||||
Qyzylorda: 'KZ',
|
||||
Rainy_River: 'CA',
|
||||
Rankin_Inlet: 'CA',
|
||||
Rarotonga: 'CK',
|
||||
Recife: 'BR',
|
||||
Regina: 'CA',
|
||||
Resolute: 'CA',
|
||||
Reunion: 'RE',
|
||||
Reykjavik: 'IS',
|
||||
Riga: 'LV',
|
||||
Rio_Branco: 'BR',
|
||||
Rio_Gallegos: 'AR',
|
||||
Riyadh: 'SA',
|
||||
Rome: 'IT',
|
||||
Rothera: 'AQ',
|
||||
Saipan: 'MP',
|
||||
Sakhalin: 'RU',
|
||||
Salta: 'AR',
|
||||
Samara: 'RU',
|
||||
Samarkand: 'UZ',
|
||||
San_Juan: 'AR',
|
||||
San_Luis: 'AR',
|
||||
San_Marino: 'SM',
|
||||
Santarem: 'BR',
|
||||
Santiago: 'CL',
|
||||
Santo_Domingo: 'DO',
|
||||
Sao_Paulo: 'BR',
|
||||
Sao_Tome: 'ST',
|
||||
Sarajevo: 'BA',
|
||||
Saratov: 'RU',
|
||||
Scoresbysund: 'GL',
|
||||
Seoul: 'KR',
|
||||
Shanghai: 'CN',
|
||||
Simferopol: 'RU',
|
||||
Singapore: 'SG',
|
||||
Sitka: 'US',
|
||||
Skopje: 'MK',
|
||||
Sofia: 'BG',
|
||||
South_Georgia: 'GS',
|
||||
Srednekolymsk: 'RU',
|
||||
St_Barthelemy: 'BL',
|
||||
St_Helena: 'SH',
|
||||
St_Johns: 'CA',
|
||||
St_Kitts: 'KN',
|
||||
St_Lucia: 'LC',
|
||||
St_Thomas: 'VI',
|
||||
St_Vincent: 'VC',
|
||||
Stanley: 'FK',
|
||||
Stockholm: 'SE',
|
||||
Swift_Current: 'CA',
|
||||
Sydney: 'AU',
|
||||
Syowa: 'AQ',
|
||||
Tahiti: 'PF',
|
||||
Taipei: 'TW',
|
||||
Tallinn: 'EE',
|
||||
Tarawa: 'KI',
|
||||
Tashkent: 'UZ',
|
||||
Tbilisi: 'GE',
|
||||
Tegucigalpa: 'HN',
|
||||
Tehran: 'IR',
|
||||
Tell_City: 'US',
|
||||
Thimphu: 'BT',
|
||||
Thule: 'GL',
|
||||
Thunder_Bay: 'CA',
|
||||
Tijuana: 'MX',
|
||||
Tirane: 'AL',
|
||||
Tokyo: 'JP',
|
||||
Tomsk: 'RU',
|
||||
Tongatapu: 'TO',
|
||||
Toronto: 'CA',
|
||||
Tortola: 'VI (UK)',
|
||||
Tripoli: 'LY',
|
||||
Troll: 'AQ',
|
||||
Tucuman: 'AR',
|
||||
Tunis: 'TN',
|
||||
Ulaanbaatar: 'MN',
|
||||
Ulyanovsk: 'RU',
|
||||
Urumqi: 'CN',
|
||||
Ushuaia: 'AR',
|
||||
'Ust-Nera': 'RU',
|
||||
Uzhgorod: 'UA',
|
||||
Vaduz: 'LI',
|
||||
Vancouver: 'CA',
|
||||
Vatican: 'VA',
|
||||
Vevay: 'US',
|
||||
Vienna: 'AT',
|
||||
Vientiane: 'LA',
|
||||
Vilnius: 'LT',
|
||||
Vincennes: 'US',
|
||||
Vladivostok: 'RU',
|
||||
Volgograd: 'RU',
|
||||
Vostok: 'AQ',
|
||||
Wake: 'UM',
|
||||
Wallis: 'WF',
|
||||
Warsaw: 'PL',
|
||||
Whitehorse: 'CA',
|
||||
Winamac: 'US',
|
||||
Windhoek: 'NA',
|
||||
Winnipeg: 'CA',
|
||||
Yakutat: 'US',
|
||||
Yakutsk: 'RU',
|
||||
Yangon: 'MM',
|
||||
Yekaterinburg: 'RU',
|
||||
Yellowknife: 'CA',
|
||||
Yerevan: 'AM',
|
||||
Zagreb: 'HR',
|
||||
Zaporozhye: 'UA',
|
||||
Zurich: 'CH'
|
||||
};
|
@ -1,7 +1,7 @@
|
||||
import type { AccessWithGranteeUser } from './access-with-grantee-user.type';
|
||||
import { AccountWithPlatform } from './account-with-platform.type';
|
||||
import { AccountWithValue } from './account-with-value.type';
|
||||
import type { ColorScheme } from './color-scheme';
|
||||
import type { ColorScheme } from './color-scheme.type';
|
||||
import type { DateRange } from './date-range.type';
|
||||
import type { Granularity } from './granularity.type';
|
||||
import { GroupBy } from './group-by.type';
|
||||
|
@ -6,6 +6,56 @@
|
||||
(valueChanged)="filters$.next($event)"
|
||||
></gf-activities-filter>
|
||||
|
||||
<div *ngIf="hasPermissionToCreateActivity" class="d-flex justify-content-end">
|
||||
<button
|
||||
class="align-items-center d-flex"
|
||||
mat-stroked-button
|
||||
(click)="onImport()"
|
||||
>
|
||||
<ion-icon class="mr-2" name="cloud-upload-outline"></ion-icon>
|
||||
<span i18n>Import Activities...</span>
|
||||
</button>
|
||||
<button
|
||||
*ngIf="hasPermissionToExportActivities"
|
||||
class="mx-1 no-min-width px-2"
|
||||
mat-stroked-button
|
||||
[matMenuTriggerFor]="activitiesMenu"
|
||||
(click)="$event.stopPropagation()"
|
||||
>
|
||||
<ion-icon name="ellipsis-vertical"></ion-icon>
|
||||
</button>
|
||||
<mat-menu #activitiesMenu="matMenu" xPosition="before">
|
||||
<button
|
||||
mat-menu-item
|
||||
[disabled]="dataSource.data.length === 0"
|
||||
(click)="onImportDividends()"
|
||||
>
|
||||
<ion-icon class="mr-2" name="color-wand-outline"></ion-icon>
|
||||
<span i18n>Import Dividends...</span>
|
||||
</button>
|
||||
<button
|
||||
*ngIf="hasPermissionToExportActivities"
|
||||
class="align-items-center d-flex"
|
||||
mat-menu-item
|
||||
[disabled]="dataSource.data.length === 0"
|
||||
(click)="onExport()"
|
||||
>
|
||||
<ion-icon class="mr-2" name="cloud-download-outline"></ion-icon>
|
||||
<span i18n>Export Activities</span>
|
||||
</button>
|
||||
<button
|
||||
*ngIf="hasPermissionToExportActivities"
|
||||
class="align-items-center d-flex"
|
||||
mat-menu-item
|
||||
[disabled]="!hasDrafts"
|
||||
(click)="onExportDrafts()"
|
||||
>
|
||||
<ion-icon class="mr-2" name="calendar-clear-outline"></ion-icon>
|
||||
<span i18n>Export Drafts as ICS</span>
|
||||
</button>
|
||||
</mat-menu>
|
||||
</div>
|
||||
|
||||
<div class="activities">
|
||||
<table
|
||||
class="gf-table w-100"
|
||||
@ -369,7 +419,7 @@
|
||||
<th *matHeaderCellDef class="px-1 text-center" mat-header-cell>
|
||||
<button
|
||||
*ngIf="
|
||||
hasPermissionToExportActivities || hasPermissionToImportActivities
|
||||
!hasPermissionToCreateActivity && hasPermissionToExportActivities
|
||||
"
|
||||
class="mx-1 no-min-width px-2"
|
||||
mat-button
|
||||
@ -380,21 +430,22 @@
|
||||
</button>
|
||||
<mat-menu #activitiesMenu="matMenu" xPosition="before">
|
||||
<button
|
||||
*ngIf="hasPermissionToImportActivities"
|
||||
*ngIf="hasPermissionToCreateActivity"
|
||||
class="align-items-center d-flex"
|
||||
mat-menu-item
|
||||
(click)="onImport()"
|
||||
>
|
||||
<ion-icon class="mr-2" name="cloud-upload-outline"></ion-icon>
|
||||
<span i18n>Import Activities</span>
|
||||
<span i18n>Import Activities...</span>
|
||||
</button>
|
||||
<button
|
||||
*ngIf="hasPermissionToImportActivities"
|
||||
*ngIf="hasPermissionToCreateActivity"
|
||||
mat-menu-item
|
||||
[disabled]="dataSource.data.length === 0"
|
||||
(click)="onImportDividends()"
|
||||
>
|
||||
<ion-icon class="mr-2" name="color-wand-outline"></ion-icon>
|
||||
<span i18n>Import Dividends</span>
|
||||
<span i18n>Import Dividends...</span>
|
||||
</button>
|
||||
<button
|
||||
*ngIf="hasPermissionToExportActivities"
|
||||
|
@ -40,7 +40,6 @@ export class ActivitiesTableComponent implements OnChanges, OnDestroy {
|
||||
@Input() hasPermissionToCreateActivity: boolean;
|
||||
@Input() hasPermissionToExportActivities: boolean;
|
||||
@Input() hasPermissionToFilter = true;
|
||||
@Input() hasPermissionToImportActivities: boolean;
|
||||
@Input() hasPermissionToOpenDetails = true;
|
||||
@Input() locale: string;
|
||||
@Input() pageSize = DEFAULT_PAGE_SIZE;
|
||||
|
@ -61,7 +61,7 @@ export class FireCalculatorComponent
|
||||
principalInvestmentAmount: new FormControl<number>(undefined),
|
||||
time: new FormControl<number>(undefined)
|
||||
});
|
||||
public chart: Chart;
|
||||
public chart: Chart<'bar'>;
|
||||
public isLoading = true;
|
||||
public projectedTotalAmount: number;
|
||||
|
||||
|
@ -5,6 +5,8 @@ const locales = {
|
||||
ASSET_CLASS: $localize`Asset Class`,
|
||||
ASSET_SUB_CLASS: $localize`Asset Sub Class`,
|
||||
CORE: $localize`Core`,
|
||||
DATA_IMPORT_AND_EXPORT_TOOLTIP_OSS: $localize`Switch to Ghostfolio Premium easily`,
|
||||
DATA_IMPORT_AND_EXPORT_TOOLTIP_PREMIUM: $localize`Switch to Ghostfolio Open Source easily`,
|
||||
EMERGENCY_FUND: $localize`Emergency Fund`,
|
||||
GRANT: $localize`Grant`,
|
||||
HIGHER_RISK: $localize`Higher Risk`,
|
||||
|
@ -66,7 +66,7 @@ export class LineChartComponent implements AfterViewInit, OnChanges, OnDestroy {
|
||||
|
||||
@ViewChild('chartCanvas') chartCanvas;
|
||||
|
||||
public chart: Chart;
|
||||
public chart: Chart<'line'>;
|
||||
public isLoading = true;
|
||||
|
||||
private readonly ANIMATION_DURATION = 1200;
|
||||
|
@ -55,7 +55,7 @@ export class PortfolioProportionChartComponent
|
||||
|
||||
@ViewChild('chartCanvas') chartCanvas: ElementRef<HTMLCanvasElement>;
|
||||
|
||||
public chart: Chart;
|
||||
public chart: Chart<'pie'>;
|
||||
public isLoading = true;
|
||||
|
||||
private readonly OTHER_KEY = 'OTHER';
|
||||
|
32
package.json
32
package.json
@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "ghostfolio",
|
||||
"version": "1.231.0",
|
||||
"version": "1.234.0",
|
||||
"homepage": "https://ghostfol.io",
|
||||
"license": "AGPL-3.0",
|
||||
"scripts": {
|
||||
@ -81,7 +81,7 @@
|
||||
"@nestjs/schedule": "2.1.0",
|
||||
"@nestjs/serve-static": "3.0.0",
|
||||
"@nrwl/angular": "15.6.3",
|
||||
"@prisma/client": "4.9.0",
|
||||
"@prisma/client": "4.10.1",
|
||||
"@simplewebauthn/browser": "5.2.1",
|
||||
"@simplewebauthn/server": "5.2.1",
|
||||
"@stripe/stripe-js": "1.22.0",
|
||||
@ -92,9 +92,9 @@
|
||||
"bull": "4.10.2",
|
||||
"cache-manager": "3.4.3",
|
||||
"cache-manager-redis-store": "2.0.0",
|
||||
"chart.js": "4.0.1",
|
||||
"chartjs-adapter-date-fns": "2.0.1",
|
||||
"chartjs-plugin-annotation": "2.1.0",
|
||||
"chart.js": "4.2.0",
|
||||
"chartjs-adapter-date-fns": "3.0.0",
|
||||
"chartjs-plugin-annotation": "2.1.2",
|
||||
"chartjs-plugin-datalabels": "2.2.0",
|
||||
"cheerio": "1.0.0-rc.12",
|
||||
"class-transformer": "0.3.2",
|
||||
@ -106,19 +106,20 @@
|
||||
"envalid": "7.3.1",
|
||||
"google-spreadsheet": "3.2.0",
|
||||
"http-status-codes": "2.2.0",
|
||||
"ionicons": "6.0.4",
|
||||
"ionicons": "6.1.2",
|
||||
"lodash": "4.17.21",
|
||||
"marked": "4.2.12",
|
||||
"ms": "3.0.0-canary.1",
|
||||
"ng-extract-i18n-merge": "2.1.2",
|
||||
"ng-extract-i18n-merge": "2.5.0",
|
||||
"ngx-device-detector": "3.0.0",
|
||||
"ngx-markdown": "14.0.1",
|
||||
"ngx-markdown": "15.1.0",
|
||||
"ngx-skeleton-loader": "5.0.0",
|
||||
"ngx-stripe": "13.0.0",
|
||||
"papaparse": "5.3.1",
|
||||
"passport": "0.6.0",
|
||||
"passport-google-oauth20": "2.0.0",
|
||||
"passport-jwt": "4.0.0",
|
||||
"prisma": "4.9.0",
|
||||
"prisma": "4.10.1",
|
||||
"reflect-metadata": "0.1.13",
|
||||
"rxjs": "7.5.6",
|
||||
"stripe": "8.199.0",
|
||||
@ -161,17 +162,18 @@
|
||||
"@types/google-spreadsheet": "3.1.5",
|
||||
"@types/jest": "28.1.8",
|
||||
"@types/lodash": "4.14.191",
|
||||
"@types/marked": "4.0.8",
|
||||
"@types/node": "18.11.18",
|
||||
"@types/papaparse": "5.3.7",
|
||||
"@types/passport-google-oauth20": "2.0.11",
|
||||
"@typescript-eslint/eslint-plugin": "5.4.0",
|
||||
"@typescript-eslint/parser": "5.4.0",
|
||||
"@typescript-eslint/eslint-plugin": "5.51.0",
|
||||
"@typescript-eslint/parser": "5.51.0",
|
||||
"codelyzer": "6.0.1",
|
||||
"cypress": "6.2.1",
|
||||
"eslint": "8.3.0",
|
||||
"eslint-config-prettier": "8.3.0",
|
||||
"eslint": "8.33.0",
|
||||
"eslint-config-prettier": "8.6.0",
|
||||
"eslint-plugin-cypress": "2.12.1",
|
||||
"eslint-plugin-import": "2.25.3",
|
||||
"eslint-plugin-import": "2.27.5",
|
||||
"import-sort-cli": "6.0.0",
|
||||
"import-sort-parser-typescript": "6.0.0",
|
||||
"import-sort-style-module": "6.0.0",
|
||||
@ -179,7 +181,7 @@
|
||||
"jest-environment-jsdom": "29.4.1",
|
||||
"jest-preset-angular": "12.2.3",
|
||||
"nx": "15.6.3",
|
||||
"prettier": "2.8.1",
|
||||
"prettier": "2.8.4",
|
||||
"prettier-plugin-organize-attributes": "0.0.5",
|
||||
"replace-in-file": "6.3.5",
|
||||
"rimraf": "3.0.2",
|
||||
|
@ -0,0 +1,2 @@
|
||||
-- AlterTable
|
||||
ALTER TABLE "Analytics" ADD COLUMN "country" TEXT;
|
@ -41,6 +41,7 @@ model Account {
|
||||
|
||||
model Analytics {
|
||||
activityCount Int @default(0)
|
||||
country String?
|
||||
updatedAt DateTime @updatedAt
|
||||
userId String @id
|
||||
User User @relation(fields: [userId], references: [id])
|
||||
|
48
test/import/ok-without-accounts.json
Normal file
48
test/import/ok-without-accounts.json
Normal file
@ -0,0 +1,48 @@
|
||||
{
|
||||
"meta": {
|
||||
"date": "2022-04-01T00:00:00.000Z",
|
||||
"version": "dev"
|
||||
},
|
||||
"activities": [
|
||||
{
|
||||
"fee": 0,
|
||||
"quantity": 0,
|
||||
"type": "BUY",
|
||||
"unitPrice": 0,
|
||||
"currency": "USD",
|
||||
"dataSource": "YAHOO",
|
||||
"date": "2050-06-05T22:00:00.000Z",
|
||||
"symbol": "MSFT"
|
||||
},
|
||||
{
|
||||
"fee": 0,
|
||||
"quantity": 1,
|
||||
"type": "ITEM",
|
||||
"unitPrice": 500000,
|
||||
"currency": "USD",
|
||||
"dataSource": "MANUAL",
|
||||
"date": "2021-12-31T22:00:00.000Z",
|
||||
"symbol": "Penthouse Apartment"
|
||||
},
|
||||
{
|
||||
"fee": 0,
|
||||
"quantity": 5,
|
||||
"type": "DIVIDEND",
|
||||
"unitPrice": 0.62,
|
||||
"currency": "USD",
|
||||
"dataSource": "YAHOO",
|
||||
"date": "2021-11-16T22:00:00.000Z",
|
||||
"symbol": "MSFT"
|
||||
},
|
||||
{
|
||||
"fee": 19,
|
||||
"quantity": 5,
|
||||
"type": "BUY",
|
||||
"unitPrice": 298.58,
|
||||
"currency": "USD",
|
||||
"dataSource": "YAHOO",
|
||||
"date": "2021-09-15T22:00:00.000Z",
|
||||
"symbol": "MSFT"
|
||||
}
|
||||
]
|
||||
}
|
@ -1,10 +1,23 @@
|
||||
{
|
||||
"meta": {
|
||||
"date": "2022-04-01T00:00:00.000Z",
|
||||
"date": "2023-02-05T00:00:00.000Z",
|
||||
"version": "dev"
|
||||
},
|
||||
"accounts": [
|
||||
{
|
||||
"accountType": "SECURITIES",
|
||||
"balance": 2000,
|
||||
"currency": "USD",
|
||||
"id": "b2d3fe1d-d6a8-41a3-be39-07ef5e9480f0",
|
||||
"isExcluded": false,
|
||||
"name": "My Online Trading Account",
|
||||
"platformId": null
|
||||
}
|
||||
],
|
||||
"activities": [
|
||||
{
|
||||
"accountId": "b2d3fe1d-d6a8-41a3-be39-07ef5e9480f0",
|
||||
"comment": null,
|
||||
"fee": 0,
|
||||
"quantity": 0,
|
||||
"type": "BUY",
|
||||
@ -15,6 +28,8 @@
|
||||
"symbol": "MSFT"
|
||||
},
|
||||
{
|
||||
"accountId": null,
|
||||
"comment": null,
|
||||
"fee": 0,
|
||||
"quantity": 1,
|
||||
"type": "ITEM",
|
||||
@ -25,6 +40,8 @@
|
||||
"symbol": "Penthouse Apartment"
|
||||
},
|
||||
{
|
||||
"accountId": "b2d3fe1d-d6a8-41a3-be39-07ef5e9480f0",
|
||||
"comment": null,
|
||||
"fee": 0,
|
||||
"quantity": 5,
|
||||
"type": "DIVIDEND",
|
||||
@ -35,6 +52,8 @@
|
||||
"symbol": "MSFT"
|
||||
},
|
||||
{
|
||||
"accountId": "b2d3fe1d-d6a8-41a3-be39-07ef5e9480f0",
|
||||
"comment": "My first order",
|
||||
"fee": 19,
|
||||
"quantity": 5,
|
||||
"type": "BUY",
|
||||
|
Reference in New Issue
Block a user