Feature/migration to accounts (#58)
* Migrate transaction table * Add accounts page * Add account page logic
This commit is contained in:
parent
e3a1d2b9cf
commit
c70eb7793e
@ -5,6 +5,12 @@ 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/),
|
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).
|
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
||||||
|
|
||||||
|
## Unreleased
|
||||||
|
|
||||||
|
### Added
|
||||||
|
|
||||||
|
- Added an account page as a preparation for the multi accounts support
|
||||||
|
|
||||||
## 0.96.0 - 30.04.2021
|
## 0.96.0 - 30.04.2021
|
||||||
|
|
||||||
### Added
|
### Added
|
||||||
|
213
apps/api/src/app/account/account.controller.ts
Normal file
213
apps/api/src/app/account/account.controller.ts
Normal file
@ -0,0 +1,213 @@
|
|||||||
|
import { RequestWithUser } from '@ghostfolio/api/app/interfaces/request-with-user.type';
|
||||||
|
import { nullifyValuesInObjects } from '@ghostfolio/api/helper/object.helper';
|
||||||
|
import { ImpersonationService } from '@ghostfolio/api/services/impersonation.service';
|
||||||
|
import { getPermissions, hasPermission, permissions } from '@ghostfolio/helper';
|
||||||
|
import {
|
||||||
|
Body,
|
||||||
|
Controller,
|
||||||
|
Delete,
|
||||||
|
Get,
|
||||||
|
Headers,
|
||||||
|
HttpException,
|
||||||
|
Inject,
|
||||||
|
Param,
|
||||||
|
Post,
|
||||||
|
Put,
|
||||||
|
UseGuards
|
||||||
|
} from '@nestjs/common';
|
||||||
|
import { REQUEST } from '@nestjs/core';
|
||||||
|
import { AuthGuard } from '@nestjs/passport';
|
||||||
|
import { Account as AccountModel } from '@prisma/client';
|
||||||
|
import { StatusCodes, getReasonPhrase } from 'http-status-codes';
|
||||||
|
|
||||||
|
import { AccountService } from './account.service';
|
||||||
|
import { CreateAccountDto } from './create-account.dto';
|
||||||
|
import { UpdateAccountDto } from './update-account.dto';
|
||||||
|
|
||||||
|
@Controller('account')
|
||||||
|
export class AccountController {
|
||||||
|
public constructor(
|
||||||
|
private readonly accountService: AccountService,
|
||||||
|
private readonly impersonationService: ImpersonationService,
|
||||||
|
@Inject(REQUEST) private readonly request: RequestWithUser
|
||||||
|
) {}
|
||||||
|
|
||||||
|
@Delete(':id')
|
||||||
|
@UseGuards(AuthGuard('jwt'))
|
||||||
|
public async deleteAccount(@Param('id') id: string): Promise<AccountModel> {
|
||||||
|
if (
|
||||||
|
!hasPermission(
|
||||||
|
getPermissions(this.request.user.role),
|
||||||
|
permissions.deleteAccount
|
||||||
|
)
|
||||||
|
) {
|
||||||
|
throw new HttpException(
|
||||||
|
getReasonPhrase(StatusCodes.FORBIDDEN),
|
||||||
|
StatusCodes.FORBIDDEN
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return this.accountService.deleteAccount(
|
||||||
|
{
|
||||||
|
id_userId: {
|
||||||
|
id,
|
||||||
|
userId: this.request.user.id
|
||||||
|
}
|
||||||
|
},
|
||||||
|
this.request.user.id
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Get()
|
||||||
|
@UseGuards(AuthGuard('jwt'))
|
||||||
|
public async getAllAccounts(
|
||||||
|
@Headers('impersonation-id') impersonationId
|
||||||
|
): Promise<AccountModel[]> {
|
||||||
|
const impersonationUserId = await this.impersonationService.validateImpersonationId(
|
||||||
|
impersonationId,
|
||||||
|
this.request.user.id
|
||||||
|
);
|
||||||
|
|
||||||
|
let accounts = await this.accountService.accounts({
|
||||||
|
include: { Platform: true },
|
||||||
|
orderBy: { name: 'desc' },
|
||||||
|
where: { userId: impersonationUserId || this.request.user.id }
|
||||||
|
});
|
||||||
|
|
||||||
|
if (
|
||||||
|
impersonationUserId &&
|
||||||
|
!hasPermission(
|
||||||
|
getPermissions(this.request.user.role),
|
||||||
|
permissions.readForeignPortfolio
|
||||||
|
)
|
||||||
|
) {
|
||||||
|
accounts = nullifyValuesInObjects(accounts, [
|
||||||
|
'fee',
|
||||||
|
'quantity',
|
||||||
|
'unitPrice'
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
return accounts;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Get(':id')
|
||||||
|
@UseGuards(AuthGuard('jwt'))
|
||||||
|
public async getAccountById(@Param('id') id: string): Promise<AccountModel> {
|
||||||
|
return this.accountService.account({
|
||||||
|
id_userId: {
|
||||||
|
id,
|
||||||
|
userId: this.request.user.id
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
@Post()
|
||||||
|
@UseGuards(AuthGuard('jwt'))
|
||||||
|
public async createAccount(
|
||||||
|
@Body() data: CreateAccountDto
|
||||||
|
): Promise<AccountModel> {
|
||||||
|
if (
|
||||||
|
!hasPermission(
|
||||||
|
getPermissions(this.request.user.role),
|
||||||
|
permissions.createAccount
|
||||||
|
)
|
||||||
|
) {
|
||||||
|
throw new HttpException(
|
||||||
|
getReasonPhrase(StatusCodes.FORBIDDEN),
|
||||||
|
StatusCodes.FORBIDDEN
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (data.platformId) {
|
||||||
|
const platformId = data.platformId;
|
||||||
|
delete data.platformId;
|
||||||
|
|
||||||
|
return this.accountService.createAccount(
|
||||||
|
{
|
||||||
|
...data,
|
||||||
|
Platform: { connect: { id: platformId } },
|
||||||
|
User: { connect: { id: this.request.user.id } }
|
||||||
|
},
|
||||||
|
this.request.user.id
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
delete data.platformId;
|
||||||
|
|
||||||
|
return this.accountService.createAccount(
|
||||||
|
{
|
||||||
|
...data,
|
||||||
|
User: { connect: { id: this.request.user.id } }
|
||||||
|
},
|
||||||
|
this.request.user.id
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Put(':id')
|
||||||
|
@UseGuards(AuthGuard('jwt'))
|
||||||
|
public async update(@Param('id') id: string, @Body() data: UpdateAccountDto) {
|
||||||
|
if (
|
||||||
|
!hasPermission(
|
||||||
|
getPermissions(this.request.user.role),
|
||||||
|
permissions.updateAccount
|
||||||
|
)
|
||||||
|
) {
|
||||||
|
throw new HttpException(
|
||||||
|
getReasonPhrase(StatusCodes.FORBIDDEN),
|
||||||
|
StatusCodes.FORBIDDEN
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const originalAccount = await this.accountService.account({
|
||||||
|
id_userId: {
|
||||||
|
id,
|
||||||
|
userId: this.request.user.id
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
if (data.platformId) {
|
||||||
|
const platformId = data.platformId;
|
||||||
|
delete data.platformId;
|
||||||
|
|
||||||
|
return this.accountService.updateAccount(
|
||||||
|
{
|
||||||
|
data: {
|
||||||
|
...data,
|
||||||
|
Platform: { connect: { id: platformId } },
|
||||||
|
User: { connect: { id: this.request.user.id } }
|
||||||
|
},
|
||||||
|
where: {
|
||||||
|
id_userId: {
|
||||||
|
id,
|
||||||
|
userId: this.request.user.id
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
this.request.user.id
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
// platformId is null, remove it
|
||||||
|
delete data.platformId;
|
||||||
|
|
||||||
|
return this.accountService.updateAccount(
|
||||||
|
{
|
||||||
|
data: {
|
||||||
|
...data,
|
||||||
|
Platform: originalAccount.platformId
|
||||||
|
? { disconnect: true }
|
||||||
|
: undefined,
|
||||||
|
User: { connect: { id: this.request.user.id } }
|
||||||
|
},
|
||||||
|
where: {
|
||||||
|
id_userId: {
|
||||||
|
id,
|
||||||
|
userId: this.request.user.id
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
this.request.user.id
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
30
apps/api/src/app/account/account.module.ts
Normal file
30
apps/api/src/app/account/account.module.ts
Normal file
@ -0,0 +1,30 @@
|
|||||||
|
import { ConfigurationService } from '@ghostfolio/api/services/configuration.service';
|
||||||
|
import { DataProviderService } from '@ghostfolio/api/services/data-provider.service';
|
||||||
|
import { AlphaVantageService } from '@ghostfolio/api/services/data-provider/alpha-vantage/alpha-vantage.service';
|
||||||
|
import { GhostfolioScraperApiService } from '@ghostfolio/api/services/data-provider/ghostfolio-scraper-api/ghostfolio-scraper-api.service';
|
||||||
|
import { RakutenRapidApiService } from '@ghostfolio/api/services/data-provider/rakuten-rapid-api/rakuten-rapid-api.service';
|
||||||
|
import { YahooFinanceService } from '@ghostfolio/api/services/data-provider/yahoo-finance/yahoo-finance.service';
|
||||||
|
import { ImpersonationService } from '@ghostfolio/api/services/impersonation.service';
|
||||||
|
import { PrismaService } from '@ghostfolio/api/services/prisma.service';
|
||||||
|
import { Module } from '@nestjs/common';
|
||||||
|
|
||||||
|
import { RedisCacheModule } from '../redis-cache/redis-cache.module';
|
||||||
|
import { AccountController } from './account.controller';
|
||||||
|
import { AccountService } from './account.service';
|
||||||
|
|
||||||
|
@Module({
|
||||||
|
imports: [RedisCacheModule],
|
||||||
|
controllers: [AccountController],
|
||||||
|
providers: [
|
||||||
|
AccountService,
|
||||||
|
AlphaVantageService,
|
||||||
|
ConfigurationService,
|
||||||
|
DataProviderService,
|
||||||
|
GhostfolioScraperApiService,
|
||||||
|
ImpersonationService,
|
||||||
|
PrismaService,
|
||||||
|
RakutenRapidApiService,
|
||||||
|
YahooFinanceService
|
||||||
|
]
|
||||||
|
})
|
||||||
|
export class AccountModule {}
|
75
apps/api/src/app/account/account.service.ts
Normal file
75
apps/api/src/app/account/account.service.ts
Normal file
@ -0,0 +1,75 @@
|
|||||||
|
import { PrismaService } from '@ghostfolio/api/services/prisma.service';
|
||||||
|
import { Injectable } from '@nestjs/common';
|
||||||
|
import { Account, Prisma } from '@prisma/client';
|
||||||
|
|
||||||
|
import { RedisCacheService } from '../redis-cache/redis-cache.service';
|
||||||
|
|
||||||
|
@Injectable()
|
||||||
|
export class AccountService {
|
||||||
|
public constructor(
|
||||||
|
private readonly redisCacheService: RedisCacheService,
|
||||||
|
private prisma: PrismaService
|
||||||
|
) {}
|
||||||
|
|
||||||
|
public async account(
|
||||||
|
accountWhereUniqueInput: Prisma.AccountWhereUniqueInput
|
||||||
|
): Promise<Account | null> {
|
||||||
|
return this.prisma.account.findUnique({
|
||||||
|
where: accountWhereUniqueInput
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
public async accounts(params: {
|
||||||
|
include?: Prisma.AccountInclude;
|
||||||
|
skip?: number;
|
||||||
|
take?: number;
|
||||||
|
cursor?: Prisma.AccountWhereUniqueInput;
|
||||||
|
where?: Prisma.AccountWhereInput;
|
||||||
|
orderBy?: Prisma.AccountOrderByInput;
|
||||||
|
}): Promise<Account[]> {
|
||||||
|
const { include, skip, take, cursor, where, orderBy } = params;
|
||||||
|
|
||||||
|
return this.prisma.account.findMany({
|
||||||
|
cursor,
|
||||||
|
include,
|
||||||
|
orderBy,
|
||||||
|
skip,
|
||||||
|
take,
|
||||||
|
where
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
public async createAccount(
|
||||||
|
data: Prisma.AccountCreateInput,
|
||||||
|
aUserId: string
|
||||||
|
): Promise<Account> {
|
||||||
|
return this.prisma.account.create({
|
||||||
|
data
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
public async deleteAccount(
|
||||||
|
where: Prisma.AccountWhereUniqueInput,
|
||||||
|
aUserId: string
|
||||||
|
): Promise<Account> {
|
||||||
|
this.redisCacheService.remove(`${aUserId}.portfolio`);
|
||||||
|
|
||||||
|
return this.prisma.account.delete({
|
||||||
|
where
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
public async updateAccount(
|
||||||
|
params: {
|
||||||
|
where: Prisma.AccountWhereUniqueInput;
|
||||||
|
data: Prisma.AccountUpdateInput;
|
||||||
|
},
|
||||||
|
aUserId: string
|
||||||
|
): Promise<Account> {
|
||||||
|
const { data, where } = params;
|
||||||
|
return this.prisma.account.update({
|
||||||
|
data,
|
||||||
|
where
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
35
apps/api/src/app/account/create-account.dto.ts
Normal file
35
apps/api/src/app/account/create-account.dto.ts
Normal file
@ -0,0 +1,35 @@
|
|||||||
|
import { Currency, DataSource, Type } from '@prisma/client';
|
||||||
|
import { IsISO8601, IsNumber, IsString, ValidateIf } from 'class-validator';
|
||||||
|
|
||||||
|
export class CreateAccountDto {
|
||||||
|
@IsString()
|
||||||
|
accountId: string;
|
||||||
|
|
||||||
|
@IsString()
|
||||||
|
currency: Currency;
|
||||||
|
|
||||||
|
@IsString()
|
||||||
|
dataSource: DataSource;
|
||||||
|
|
||||||
|
@IsISO8601()
|
||||||
|
date: string;
|
||||||
|
|
||||||
|
@IsNumber()
|
||||||
|
fee: number;
|
||||||
|
|
||||||
|
@IsString()
|
||||||
|
@ValidateIf((object, value) => value !== null)
|
||||||
|
platformId: string | null;
|
||||||
|
|
||||||
|
@IsNumber()
|
||||||
|
quantity: number;
|
||||||
|
|
||||||
|
@IsString()
|
||||||
|
symbol: string;
|
||||||
|
|
||||||
|
@IsString()
|
||||||
|
type: Type;
|
||||||
|
|
||||||
|
@IsNumber()
|
||||||
|
unitPrice: number;
|
||||||
|
}
|
@ -0,0 +1,3 @@
|
|||||||
|
import { Order, Platform } from '@prisma/client';
|
||||||
|
|
||||||
|
export type OrderWithPlatform = Order & { Platform?: Platform };
|
38
apps/api/src/app/account/update-account.dto.ts
Normal file
38
apps/api/src/app/account/update-account.dto.ts
Normal file
@ -0,0 +1,38 @@
|
|||||||
|
import { Currency, DataSource, Type } from '@prisma/client';
|
||||||
|
import { IsISO8601, IsNumber, IsString, ValidateIf } from 'class-validator';
|
||||||
|
|
||||||
|
export class UpdateAccountDto {
|
||||||
|
@IsString()
|
||||||
|
accountId: string;
|
||||||
|
|
||||||
|
@IsString()
|
||||||
|
currency: Currency;
|
||||||
|
|
||||||
|
@IsString()
|
||||||
|
dataSource: DataSource;
|
||||||
|
|
||||||
|
@IsISO8601()
|
||||||
|
date: string;
|
||||||
|
|
||||||
|
@IsNumber()
|
||||||
|
fee: number;
|
||||||
|
|
||||||
|
@IsString()
|
||||||
|
@ValidateIf((object, value) => value !== null)
|
||||||
|
platformId: string | null;
|
||||||
|
|
||||||
|
@IsString()
|
||||||
|
id: string;
|
||||||
|
|
||||||
|
@IsNumber()
|
||||||
|
quantity: number;
|
||||||
|
|
||||||
|
@IsString()
|
||||||
|
symbol: string;
|
||||||
|
|
||||||
|
@IsString()
|
||||||
|
type: Type;
|
||||||
|
|
||||||
|
@IsNumber()
|
||||||
|
unitPrice: number;
|
||||||
|
}
|
@ -16,6 +16,7 @@ import { YahooFinanceService } from '../services/data-provider/yahoo-finance/yah
|
|||||||
import { ExchangeRateDataService } from '../services/exchange-rate-data.service';
|
import { ExchangeRateDataService } from '../services/exchange-rate-data.service';
|
||||||
import { PrismaService } from '../services/prisma.service';
|
import { PrismaService } from '../services/prisma.service';
|
||||||
import { AccessModule } from './access/access.module';
|
import { AccessModule } from './access/access.module';
|
||||||
|
import { AccountModule } from './account/account.module';
|
||||||
import { AdminModule } from './admin/admin.module';
|
import { AdminModule } from './admin/admin.module';
|
||||||
import { AppController } from './app.controller';
|
import { AppController } from './app.controller';
|
||||||
import { AuthModule } from './auth/auth.module';
|
import { AuthModule } from './auth/auth.module';
|
||||||
@ -32,6 +33,7 @@ import { UserModule } from './user/user.module';
|
|||||||
imports: [
|
imports: [
|
||||||
AdminModule,
|
AdminModule,
|
||||||
AccessModule,
|
AccessModule,
|
||||||
|
AccountModule,
|
||||||
AuthModule,
|
AuthModule,
|
||||||
CacheModule,
|
CacheModule,
|
||||||
ConfigModule.forRoot(),
|
ConfigModule.forRoot(),
|
||||||
|
@ -71,7 +71,11 @@ export class OrderController {
|
|||||||
|
|
||||||
let orders = await this.orderService.orders({
|
let orders = await this.orderService.orders({
|
||||||
include: {
|
include: {
|
||||||
Platform: true
|
Account: {
|
||||||
|
include: {
|
||||||
|
Platform: true
|
||||||
|
}
|
||||||
|
}
|
||||||
},
|
},
|
||||||
orderBy: { date: 'desc' },
|
orderBy: { date: 'desc' },
|
||||||
where: { userId: impersonationUserId || this.request.user.id }
|
where: { userId: impersonationUserId || this.request.user.id }
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
import { DataSource } from '.prisma/client';
|
|
||||||
import { getYesterday } from '@ghostfolio/helper';
|
import { getYesterday } from '@ghostfolio/helper';
|
||||||
import { Injectable } from '@nestjs/common';
|
import { Injectable } from '@nestjs/common';
|
||||||
|
import { DataSource } from '@prisma/client';
|
||||||
import * as bent from 'bent';
|
import * as bent from 'bent';
|
||||||
import * as cheerio from 'cheerio';
|
import * as cheerio from 'cheerio';
|
||||||
import { format } from 'date-fns';
|
import { format } from 'date-fns';
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
import { Currency } from '.prisma/client';
|
import { Currency } from '@prisma/client';
|
||||||
|
|
||||||
export interface ScraperConfig {
|
export interface ScraperConfig {
|
||||||
currency: Currency;
|
currency: Currency;
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
import { DataSource } from '.prisma/client';
|
|
||||||
import { getToday, getYesterday } from '@ghostfolio/helper';
|
import { getToday, getYesterday } from '@ghostfolio/helper';
|
||||||
import { Injectable } from '@nestjs/common';
|
import { Injectable } from '@nestjs/common';
|
||||||
|
import { DataSource } from '@prisma/client';
|
||||||
import * as bent from 'bent';
|
import * as bent from 'bent';
|
||||||
import { format, subMonths, subWeeks, subYears } from 'date-fns';
|
import { format, subMonths, subWeeks, subYears } from 'date-fns';
|
||||||
|
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
import { DataSource } from '.prisma/client';
|
|
||||||
import { isCrypto, isCurrency, parseCurrency } from '@ghostfolio/helper';
|
import { isCrypto, isCurrency, parseCurrency } from '@ghostfolio/helper';
|
||||||
import { Injectable } from '@nestjs/common';
|
import { Injectable } from '@nestjs/common';
|
||||||
|
import { DataSource } from '@prisma/client';
|
||||||
import { format } from 'date-fns';
|
import { format } from 'date-fns';
|
||||||
import * as yahooFinance from 'yahoo-finance';
|
import * as yahooFinance from 'yahoo-finance';
|
||||||
|
|
||||||
|
@ -9,11 +9,6 @@ const routes: Routes = [
|
|||||||
loadChildren: () =>
|
loadChildren: () =>
|
||||||
import('./pages/about/about-page.module').then((m) => m.AboutPageModule)
|
import('./pages/about/about-page.module').then((m) => m.AboutPageModule)
|
||||||
},
|
},
|
||||||
{
|
|
||||||
path: 'admin',
|
|
||||||
loadChildren: () =>
|
|
||||||
import('./pages/admin/admin-page.module').then((m) => m.AdminPageModule)
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
path: 'account',
|
path: 'account',
|
||||||
loadChildren: () =>
|
loadChildren: () =>
|
||||||
@ -21,6 +16,18 @@ const routes: Routes = [
|
|||||||
(m) => m.AccountPageModule
|
(m) => m.AccountPageModule
|
||||||
)
|
)
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
path: 'accounts',
|
||||||
|
loadChildren: () =>
|
||||||
|
import('./pages/accounts/accounts-page.module').then(
|
||||||
|
(m) => m.AccountsPageModule
|
||||||
|
)
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: 'admin',
|
||||||
|
loadChildren: () =>
|
||||||
|
import('./pages/admin/admin-page.module').then((m) => m.AdminPageModule)
|
||||||
|
},
|
||||||
{
|
{
|
||||||
path: 'auth',
|
path: 'auth',
|
||||||
loadChildren: () =>
|
loadChildren: () =>
|
||||||
|
@ -74,7 +74,7 @@ export class AppComponent implements OnDestroy, OnInit {
|
|||||||
|
|
||||||
this.canCreateAccount = hasPermission(
|
this.canCreateAccount = hasPermission(
|
||||||
this.user.permissions,
|
this.user.permissions,
|
||||||
permissions.createAccount
|
permissions.createUserAccount
|
||||||
);
|
);
|
||||||
|
|
||||||
this.cd.markForCheck();
|
this.cd.markForCheck();
|
||||||
|
@ -0,0 +1,86 @@
|
|||||||
|
<table
|
||||||
|
class="w-100"
|
||||||
|
matSort
|
||||||
|
matSortActive="account"
|
||||||
|
matSortDirection="desc"
|
||||||
|
mat-table
|
||||||
|
[dataSource]="dataSource"
|
||||||
|
>
|
||||||
|
<ng-container matColumnDef="account">
|
||||||
|
<th *matHeaderCellDef i18n mat-header-cell mat-sort-header>Name</th>
|
||||||
|
<td *matCellDef="let element" mat-cell>
|
||||||
|
{{ element.name }}
|
||||||
|
</td>
|
||||||
|
</ng-container>
|
||||||
|
|
||||||
|
<ng-container matColumnDef="type">
|
||||||
|
<th
|
||||||
|
*matHeaderCellDef
|
||||||
|
class="d-none d-lg-table-cell justify-content-center"
|
||||||
|
i18n
|
||||||
|
mat-header-cell
|
||||||
|
mat-sort-header
|
||||||
|
>
|
||||||
|
Type
|
||||||
|
</th>
|
||||||
|
<td
|
||||||
|
mat-cell
|
||||||
|
*matCellDef="let element"
|
||||||
|
class="d-none d-lg-table-cell text-center"
|
||||||
|
>
|
||||||
|
<div class="d-inline-flex justify-content-center px-2 py-1 type-badge">
|
||||||
|
<span>{{ element.accountType }}</span>
|
||||||
|
</div>
|
||||||
|
</td>
|
||||||
|
</ng-container>
|
||||||
|
|
||||||
|
<ng-container matColumnDef="platform">
|
||||||
|
<th *matHeaderCellDef i18n mat-header-cell mat-sort-header>Platform</th>
|
||||||
|
<td mat-cell *matCellDef="let element">
|
||||||
|
<div class="d-flex">
|
||||||
|
<gf-symbol-icon
|
||||||
|
*ngIf="element.Platform?.url"
|
||||||
|
class="mr-1"
|
||||||
|
[tooltip]=""
|
||||||
|
[url]="element.Platform?.url"
|
||||||
|
></gf-symbol-icon>
|
||||||
|
<span>{{ element.Platform?.name }}</span>
|
||||||
|
</div>
|
||||||
|
</td>
|
||||||
|
</ng-container>
|
||||||
|
|
||||||
|
<ng-container matColumnDef="actions">
|
||||||
|
<th *matHeaderCellDef class="px-0 text-center" i18n mat-header-cell></th>
|
||||||
|
<td *matCellDef="let element" class="px-0 text-center" mat-cell>
|
||||||
|
<button
|
||||||
|
class="mx-1 no-min-width px-2"
|
||||||
|
mat-button
|
||||||
|
[matMenuTriggerFor]="accountMenu"
|
||||||
|
(click)="$event.stopPropagation()"
|
||||||
|
>
|
||||||
|
<ion-icon name="ellipsis-vertical"></ion-icon>
|
||||||
|
</button>
|
||||||
|
<mat-menu #accountMenu="matMenu" xPosition="before">
|
||||||
|
<button i18n mat-menu-item (click)="onUpdateAccount(element)">
|
||||||
|
Edit
|
||||||
|
</button>
|
||||||
|
<button i18n mat-menu-item (click)="onDeleteAccount(element.id)">
|
||||||
|
Delete
|
||||||
|
</button>
|
||||||
|
</mat-menu>
|
||||||
|
</td>
|
||||||
|
</ng-container>
|
||||||
|
|
||||||
|
<tr *matHeaderRowDef="displayedColumns" mat-header-row></tr>
|
||||||
|
<tr *matRowDef="let row; columns: displayedColumns" mat-row></tr>
|
||||||
|
</table>
|
||||||
|
|
||||||
|
<ngx-skeleton-loader
|
||||||
|
*ngIf="isLoading"
|
||||||
|
animation="pulse"
|
||||||
|
class="px-4 py-3"
|
||||||
|
[theme]="{
|
||||||
|
height: '1.5rem',
|
||||||
|
width: '100%'
|
||||||
|
}"
|
||||||
|
></ngx-skeleton-loader>
|
@ -0,0 +1,59 @@
|
|||||||
|
:host {
|
||||||
|
display: block;
|
||||||
|
|
||||||
|
::ng-deep {
|
||||||
|
.mat-form-field-infix {
|
||||||
|
border-top: 0 solid transparent !important;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.mat-table {
|
||||||
|
td {
|
||||||
|
border: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
th {
|
||||||
|
::ng-deep {
|
||||||
|
.mat-sort-header-container {
|
||||||
|
justify-content: inherit;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.mat-row {
|
||||||
|
&:nth-child(even) {
|
||||||
|
background-color: rgba(
|
||||||
|
var(--dark-primary-text),
|
||||||
|
var(--palette-background-hover-alpha)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
.type-badge {
|
||||||
|
background-color: rgba(var(--dark-primary-text), 0.05);
|
||||||
|
border-radius: 1rem;
|
||||||
|
line-height: 1em;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
:host-context(.is-dark-theme) {
|
||||||
|
.mat-form-field {
|
||||||
|
color: rgba(var(--light-primary-text));
|
||||||
|
}
|
||||||
|
|
||||||
|
.mat-table {
|
||||||
|
.mat-row {
|
||||||
|
&:nth-child(even) {
|
||||||
|
background-color: rgba(
|
||||||
|
var(--light-primary-text),
|
||||||
|
var(--palette-background-hover-alpha)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
.type-badge {
|
||||||
|
background-color: rgba(var(--light-primary-text), 0.1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,85 @@
|
|||||||
|
import {
|
||||||
|
ChangeDetectionStrategy,
|
||||||
|
Component,
|
||||||
|
EventEmitter,
|
||||||
|
Input,
|
||||||
|
OnChanges,
|
||||||
|
OnDestroy,
|
||||||
|
OnInit,
|
||||||
|
Output,
|
||||||
|
ViewChild
|
||||||
|
} from '@angular/core';
|
||||||
|
import { MatDialog } from '@angular/material/dialog';
|
||||||
|
import { MatSort } from '@angular/material/sort';
|
||||||
|
import { MatTableDataSource } from '@angular/material/table';
|
||||||
|
import { ActivatedRoute, Router } from '@angular/router';
|
||||||
|
import { Order as OrderModel } from '@prisma/client';
|
||||||
|
import { Subject, Subscription } from 'rxjs';
|
||||||
|
|
||||||
|
@Component({
|
||||||
|
selector: 'gf-accounts-table',
|
||||||
|
changeDetection: ChangeDetectionStrategy.OnPush,
|
||||||
|
templateUrl: './accounts-table.component.html',
|
||||||
|
styleUrls: ['./accounts-table.component.scss']
|
||||||
|
})
|
||||||
|
export class AccountsTableComponent implements OnChanges, OnDestroy, OnInit {
|
||||||
|
@Input() accounts: OrderModel[];
|
||||||
|
@Input() baseCurrency: string;
|
||||||
|
@Input() deviceType: string;
|
||||||
|
@Input() locale: string;
|
||||||
|
@Input() showActions: boolean;
|
||||||
|
|
||||||
|
@Output() accountDeleted = new EventEmitter<string>();
|
||||||
|
@Output() accountToUpdate = new EventEmitter<OrderModel>();
|
||||||
|
|
||||||
|
@ViewChild(MatSort) sort: MatSort;
|
||||||
|
|
||||||
|
public dataSource: MatTableDataSource<OrderModel> = new MatTableDataSource();
|
||||||
|
public displayedColumns = [];
|
||||||
|
public isLoading = true;
|
||||||
|
public routeQueryParams: Subscription;
|
||||||
|
|
||||||
|
private unsubscribeSubject = new Subject<void>();
|
||||||
|
|
||||||
|
public constructor(
|
||||||
|
private dialog: MatDialog,
|
||||||
|
private route: ActivatedRoute,
|
||||||
|
private router: Router
|
||||||
|
) {}
|
||||||
|
|
||||||
|
public ngOnInit() {}
|
||||||
|
|
||||||
|
public ngOnChanges() {
|
||||||
|
this.displayedColumns = ['account', 'type', 'platform'];
|
||||||
|
|
||||||
|
this.isLoading = true;
|
||||||
|
|
||||||
|
if (this.showActions) {
|
||||||
|
this.displayedColumns.push('actions');
|
||||||
|
}
|
||||||
|
|
||||||
|
if (this.accounts) {
|
||||||
|
this.dataSource = new MatTableDataSource(this.accounts);
|
||||||
|
this.dataSource.sort = this.sort;
|
||||||
|
|
||||||
|
this.isLoading = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public onDeleteAccount(aId: string) {
|
||||||
|
const confirmation = confirm('Do you really want to delete this account?');
|
||||||
|
|
||||||
|
if (confirmation) {
|
||||||
|
this.accountDeleted.emit(aId);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public onUpdateAccount(aAccount: OrderModel) {
|
||||||
|
this.accountToUpdate.emit(aAccount);
|
||||||
|
}
|
||||||
|
|
||||||
|
public ngOnDestroy() {
|
||||||
|
this.unsubscribeSubject.next();
|
||||||
|
this.unsubscribeSubject.complete();
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,33 @@
|
|||||||
|
import { CommonModule } from '@angular/common';
|
||||||
|
import { CUSTOM_ELEMENTS_SCHEMA, NgModule } from '@angular/core';
|
||||||
|
import { MatButtonModule } from '@angular/material/button';
|
||||||
|
import { MatInputModule } from '@angular/material/input';
|
||||||
|
import { MatMenuModule } from '@angular/material/menu';
|
||||||
|
import { MatSortModule } from '@angular/material/sort';
|
||||||
|
import { MatTableModule } from '@angular/material/table';
|
||||||
|
import { RouterModule } from '@angular/router';
|
||||||
|
import { NgxSkeletonLoaderModule } from 'ngx-skeleton-loader';
|
||||||
|
|
||||||
|
import { GfSymbolIconModule } from '../symbol-icon/symbol-icon.module';
|
||||||
|
import { GfValueModule } from '../value/value.module';
|
||||||
|
import { AccountsTableComponent } from './accounts-table.component';
|
||||||
|
|
||||||
|
@NgModule({
|
||||||
|
declarations: [AccountsTableComponent],
|
||||||
|
exports: [AccountsTableComponent],
|
||||||
|
imports: [
|
||||||
|
CommonModule,
|
||||||
|
GfSymbolIconModule,
|
||||||
|
GfValueModule,
|
||||||
|
MatButtonModule,
|
||||||
|
MatInputModule,
|
||||||
|
MatMenuModule,
|
||||||
|
MatSortModule,
|
||||||
|
MatTableModule,
|
||||||
|
NgxSkeletonLoaderModule,
|
||||||
|
RouterModule
|
||||||
|
],
|
||||||
|
providers: [],
|
||||||
|
schemas: [CUSTOM_ELEMENTS_SCHEMA]
|
||||||
|
})
|
||||||
|
export class GfAccountsTableModule {}
|
@ -36,6 +36,14 @@
|
|||||||
[color]="currentRoute === 'transactions' ? 'primary' : null"
|
[color]="currentRoute === 'transactions' ? 'primary' : null"
|
||||||
>Transactions</a
|
>Transactions</a
|
||||||
>
|
>
|
||||||
|
<a
|
||||||
|
class="d-none d-sm-block mx-1"
|
||||||
|
[routerLink]="['/accounts']"
|
||||||
|
i18n
|
||||||
|
mat-flat-button
|
||||||
|
[color]="currentRoute === 'accounts' ? 'primary' : null"
|
||||||
|
>Accounts</a
|
||||||
|
>
|
||||||
<a
|
<a
|
||||||
*ngIf="hasPermissionToAccessAdminControl"
|
*ngIf="hasPermissionToAccessAdminControl"
|
||||||
class="d-none d-sm-block mx-1"
|
class="d-none d-sm-block mx-1"
|
||||||
@ -141,6 +149,14 @@
|
|||||||
[ngClass]="{ 'font-weight-bold': currentRoute === 'transactions' }"
|
[ngClass]="{ 'font-weight-bold': currentRoute === 'transactions' }"
|
||||||
>Transactions</a
|
>Transactions</a
|
||||||
>
|
>
|
||||||
|
<a
|
||||||
|
class="d-block d-sm-none"
|
||||||
|
[routerLink]="['/accounts']"
|
||||||
|
i18n
|
||||||
|
mat-menu-item
|
||||||
|
[ngClass]="{ 'font-weight-bold': currentRoute === 'accounts' }"
|
||||||
|
>Accounts</a
|
||||||
|
>
|
||||||
<a
|
<a
|
||||||
class="align-items-center d-flex"
|
class="align-items-center d-flex"
|
||||||
[routerLink]="['/account']"
|
[routerLink]="['/account']"
|
||||||
|
@ -17,25 +17,21 @@
|
|||||||
mat-table
|
mat-table
|
||||||
[dataSource]="dataSource"
|
[dataSource]="dataSource"
|
||||||
>
|
>
|
||||||
<ng-container matColumnDef="platform">
|
<ng-container matColumnDef="account">
|
||||||
<th
|
<th *matHeaderCellDef i18n mat-header-cell mat-sort-header>Account</th>
|
||||||
*matHeaderCellDef
|
<td *matCellDef="let element" mat-cell>
|
||||||
class="d-none d-lg-table-cell text-center px-0"
|
<div class="d-flex">
|
||||||
i18n
|
|
||||||
mat-header-cell
|
|
||||||
>
|
|
||||||
Platform
|
|
||||||
</th>
|
|
||||||
<td *matCellDef="let element" class="d-none d-lg-table-cell px-0" mat-cell>
|
|
||||||
<div class="d-flex justify-content-center">
|
|
||||||
<gf-symbol-icon
|
<gf-symbol-icon
|
||||||
*ngIf="element.Platform?.url"
|
*ngIf="element.Account?.Platform?.url"
|
||||||
[tooltip]="element.Platform?.name"
|
class="mr-1"
|
||||||
[url]="element.Platform?.url"
|
[tooltip]="element.Account?.Platform?.name"
|
||||||
|
[url]="element.Account?.Platform?.url"
|
||||||
></gf-symbol-icon>
|
></gf-symbol-icon>
|
||||||
|
<span class="d-none d-lg-block">{{ element.Account?.name }}</span>
|
||||||
</div>
|
</div>
|
||||||
</td>
|
</td>
|
||||||
</ng-container>
|
</ng-container>
|
||||||
|
|
||||||
<ng-container matColumnDef="date">
|
<ng-container matColumnDef="date">
|
||||||
<th
|
<th
|
||||||
*matHeaderCellDef
|
*matHeaderCellDef
|
||||||
|
@ -68,7 +68,7 @@ export class TransactionsTableComponent
|
|||||||
|
|
||||||
public ngOnChanges() {
|
public ngOnChanges() {
|
||||||
this.displayedColumns = [
|
this.displayedColumns = [
|
||||||
'platform',
|
'account',
|
||||||
'date',
|
'date',
|
||||||
'type',
|
'type',
|
||||||
'symbol',
|
'symbol',
|
||||||
|
@ -8,11 +8,10 @@ import {
|
|||||||
hasPermission,
|
hasPermission,
|
||||||
permissions
|
permissions
|
||||||
} from '@ghostfolio/helper';
|
} from '@ghostfolio/helper';
|
||||||
|
import { Currency } from '@prisma/client';
|
||||||
import { Subject } from 'rxjs';
|
import { Subject } from 'rxjs';
|
||||||
import { takeUntil } from 'rxjs/operators';
|
import { takeUntil } from 'rxjs/operators';
|
||||||
|
|
||||||
import { Currency } from '.prisma/client';
|
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
selector: 'gf-account-page',
|
selector: 'gf-account-page',
|
||||||
templateUrl: './account-page.html',
|
templateUrl: './account-page.html',
|
||||||
|
@ -0,0 +1,15 @@
|
|||||||
|
import { NgModule } from '@angular/core';
|
||||||
|
import { RouterModule, Routes } from '@angular/router';
|
||||||
|
import { AuthGuard } from '@ghostfolio/client/core/auth.guard';
|
||||||
|
|
||||||
|
import { AccountsPageComponent } from './accounts-page.component';
|
||||||
|
|
||||||
|
const routes: Routes = [
|
||||||
|
{ path: '', component: AccountsPageComponent, canActivate: [AuthGuard] }
|
||||||
|
];
|
||||||
|
|
||||||
|
@NgModule({
|
||||||
|
imports: [RouterModule.forChild(routes)],
|
||||||
|
exports: [RouterModule]
|
||||||
|
})
|
||||||
|
export class AccountsPageRoutingModule {}
|
215
apps/client/src/app/pages/accounts/accounts-page.component.ts
Normal file
215
apps/client/src/app/pages/accounts/accounts-page.component.ts
Normal file
@ -0,0 +1,215 @@
|
|||||||
|
import { ChangeDetectorRef, Component, OnInit } from '@angular/core';
|
||||||
|
import { MatDialog } from '@angular/material/dialog';
|
||||||
|
import { ActivatedRoute, Router } from '@angular/router';
|
||||||
|
import { UpdateOrderDto } from '@ghostfolio/api/app/order/update-order.dto';
|
||||||
|
import { User } from '@ghostfolio/api/app/user/interfaces/user.interface';
|
||||||
|
import { DataService } from '@ghostfolio/client/services/data.service';
|
||||||
|
import { ImpersonationStorageService } from '@ghostfolio/client/services/impersonation-storage.service';
|
||||||
|
import { TokenStorageService } from '@ghostfolio/client/services/token-storage.service';
|
||||||
|
import { hasPermission, permissions } from '@ghostfolio/helper';
|
||||||
|
import { Order as OrderModel } from '@prisma/client';
|
||||||
|
import { DeviceDetectorService } from 'ngx-device-detector';
|
||||||
|
import { Subject, Subscription } from 'rxjs';
|
||||||
|
import { takeUntil } from 'rxjs/operators';
|
||||||
|
|
||||||
|
import { CreateOrUpdateAccountDialog } from './create-or-update-account-dialog/create-or-update-account-dialog.component';
|
||||||
|
|
||||||
|
@Component({
|
||||||
|
selector: 'gf-accounts-page',
|
||||||
|
templateUrl: './accounts-page.html',
|
||||||
|
styleUrls: ['./accounts-page.scss']
|
||||||
|
})
|
||||||
|
export class AccountsPageComponent implements OnInit {
|
||||||
|
public accounts: OrderModel[];
|
||||||
|
public deviceType: string;
|
||||||
|
public hasImpersonationId: boolean;
|
||||||
|
public hasPermissionToCreateAccount: boolean;
|
||||||
|
public hasPermissionToDeleteAccount: boolean;
|
||||||
|
public routeQueryParams: Subscription;
|
||||||
|
public user: User;
|
||||||
|
|
||||||
|
private unsubscribeSubject = new Subject<void>();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @constructor
|
||||||
|
*/
|
||||||
|
public constructor(
|
||||||
|
private cd: ChangeDetectorRef,
|
||||||
|
private dataService: DataService,
|
||||||
|
private deviceService: DeviceDetectorService,
|
||||||
|
private dialog: MatDialog,
|
||||||
|
private impersonationStorageService: ImpersonationStorageService,
|
||||||
|
private route: ActivatedRoute,
|
||||||
|
private router: Router,
|
||||||
|
private tokenStorageService: TokenStorageService
|
||||||
|
) {
|
||||||
|
this.routeQueryParams = route.queryParams
|
||||||
|
.pipe(takeUntil(this.unsubscribeSubject))
|
||||||
|
.subscribe((params) => {
|
||||||
|
if (params['createDialog']) {
|
||||||
|
this.openCreateAccountDialog();
|
||||||
|
} else if (params['editDialog']) {
|
||||||
|
if (this.accounts) {
|
||||||
|
const account = this.accounts.find((account) => {
|
||||||
|
return account.id === params['transactionId'];
|
||||||
|
});
|
||||||
|
|
||||||
|
this.openUpdateAccountDialog(account);
|
||||||
|
} else {
|
||||||
|
this.router.navigate(['.'], { relativeTo: this.route });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Initializes the controller
|
||||||
|
*/
|
||||||
|
public ngOnInit() {
|
||||||
|
this.deviceType = this.deviceService.getDeviceInfo().deviceType;
|
||||||
|
|
||||||
|
this.impersonationStorageService
|
||||||
|
.onChangeHasImpersonation()
|
||||||
|
.subscribe((aId) => {
|
||||||
|
this.hasImpersonationId = !!aId;
|
||||||
|
});
|
||||||
|
|
||||||
|
this.tokenStorageService
|
||||||
|
.onChangeHasToken()
|
||||||
|
.pipe(takeUntil(this.unsubscribeSubject))
|
||||||
|
.subscribe(() => {
|
||||||
|
this.dataService.fetchUser().subscribe((user) => {
|
||||||
|
this.user = user;
|
||||||
|
this.hasPermissionToCreateAccount = hasPermission(
|
||||||
|
user.permissions,
|
||||||
|
permissions.createAccount
|
||||||
|
);
|
||||||
|
this.hasPermissionToDeleteAccount = hasPermission(
|
||||||
|
user.permissions,
|
||||||
|
permissions.deleteAccount
|
||||||
|
);
|
||||||
|
|
||||||
|
this.cd.markForCheck();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
this.fetchAccounts();
|
||||||
|
}
|
||||||
|
|
||||||
|
public fetchAccounts() {
|
||||||
|
this.dataService.fetchAccounts().subscribe((response) => {
|
||||||
|
this.accounts = response;
|
||||||
|
|
||||||
|
if (this.accounts?.length <= 0) {
|
||||||
|
this.router.navigate([], { queryParams: { createDialog: true } });
|
||||||
|
}
|
||||||
|
|
||||||
|
this.cd.markForCheck();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
public onDeleteAccount(aId: string) {
|
||||||
|
this.dataService.deleteAccount(aId).subscribe({
|
||||||
|
next: () => {
|
||||||
|
this.fetchAccounts();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
public onUpdateAccount(aAccount: OrderModel) {
|
||||||
|
this.router.navigate([], {
|
||||||
|
queryParams: { editDialog: true, transactionId: aAccount.id }
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
public openUpdateAccountDialog({
|
||||||
|
accountId,
|
||||||
|
currency,
|
||||||
|
dataSource,
|
||||||
|
date,
|
||||||
|
fee,
|
||||||
|
id,
|
||||||
|
platformId,
|
||||||
|
quantity,
|
||||||
|
symbol,
|
||||||
|
type,
|
||||||
|
unitPrice
|
||||||
|
}: OrderModel): void {
|
||||||
|
const dialogRef = this.dialog.open(CreateOrUpdateAccountDialog, {
|
||||||
|
data: {
|
||||||
|
accounts: this.user.accounts,
|
||||||
|
transaction: {
|
||||||
|
accountId,
|
||||||
|
currency,
|
||||||
|
dataSource,
|
||||||
|
date,
|
||||||
|
fee,
|
||||||
|
id,
|
||||||
|
platformId,
|
||||||
|
quantity,
|
||||||
|
symbol,
|
||||||
|
type,
|
||||||
|
unitPrice
|
||||||
|
}
|
||||||
|
},
|
||||||
|
height: this.deviceType === 'mobile' ? '97.5vh' : '80vh',
|
||||||
|
width: this.deviceType === 'mobile' ? '100vw' : '50rem'
|
||||||
|
});
|
||||||
|
|
||||||
|
dialogRef.afterClosed().subscribe((data: any) => {
|
||||||
|
const transaction: UpdateOrderDto = data?.transaction;
|
||||||
|
|
||||||
|
if (transaction) {
|
||||||
|
this.dataService.putAccount(transaction).subscribe({
|
||||||
|
next: () => {
|
||||||
|
this.fetchAccounts();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
this.router.navigate(['.'], { relativeTo: this.route });
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
public ngOnDestroy() {
|
||||||
|
this.unsubscribeSubject.next();
|
||||||
|
this.unsubscribeSubject.complete();
|
||||||
|
}
|
||||||
|
|
||||||
|
private openCreateAccountDialog(): void {
|
||||||
|
const dialogRef = this.dialog.open(CreateOrUpdateAccountDialog, {
|
||||||
|
data: {
|
||||||
|
accounts: this.user?.accounts,
|
||||||
|
transaction: {
|
||||||
|
accountId: this.user?.accounts.find((account) => {
|
||||||
|
return account.isDefault;
|
||||||
|
})?.id,
|
||||||
|
currency: null,
|
||||||
|
date: new Date(),
|
||||||
|
fee: 0,
|
||||||
|
platformId: null,
|
||||||
|
quantity: null,
|
||||||
|
symbol: null,
|
||||||
|
type: 'BUY',
|
||||||
|
unitPrice: null
|
||||||
|
}
|
||||||
|
},
|
||||||
|
height: this.deviceType === 'mobile' ? '97.5vh' : '80vh',
|
||||||
|
width: this.deviceType === 'mobile' ? '100vw' : '50rem'
|
||||||
|
});
|
||||||
|
|
||||||
|
dialogRef.afterClosed().subscribe((data: any) => {
|
||||||
|
const transaction: UpdateOrderDto = data?.transaction;
|
||||||
|
|
||||||
|
if (transaction) {
|
||||||
|
this.dataService.postAccount(transaction).subscribe({
|
||||||
|
next: () => {
|
||||||
|
this.fetchAccounts();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
this.router.navigate(['.'], { relativeTo: this.route });
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
31
apps/client/src/app/pages/accounts/accounts-page.html
Normal file
31
apps/client/src/app/pages/accounts/accounts-page.html
Normal file
@ -0,0 +1,31 @@
|
|||||||
|
<div class="container">
|
||||||
|
<div class="row mb-3">
|
||||||
|
<div class="col">
|
||||||
|
<h3 class="d-flex justify-content-center mb-3" i18n>Accounts</h3>
|
||||||
|
<gf-accounts-table
|
||||||
|
[accounts]="accounts"
|
||||||
|
[baseCurrency]="user?.settings?.baseCurrency"
|
||||||
|
[deviceType]="deviceType"
|
||||||
|
[locale]="user?.settings?.locale"
|
||||||
|
[showActions]="!hasImpersonationId && hasPermissionToDeleteAccount"
|
||||||
|
(accountDeleted)="onDeleteAccount($event)"
|
||||||
|
(accountToUpdate)="onUpdateAccount($event)"
|
||||||
|
></gf-accounts-table>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div
|
||||||
|
*ngIf="!hasImpersonationId && hasPermissionToCreateAccount"
|
||||||
|
class="fab-container"
|
||||||
|
>
|
||||||
|
<a
|
||||||
|
class="align-items-center d-flex justify-content-center"
|
||||||
|
color="primary"
|
||||||
|
mat-fab
|
||||||
|
[routerLink]="[]"
|
||||||
|
[queryParams]="{ createDialog: true }"
|
||||||
|
>
|
||||||
|
<ion-icon name="add-outline" size="large"></ion-icon>
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
25
apps/client/src/app/pages/accounts/accounts-page.module.ts
Normal file
25
apps/client/src/app/pages/accounts/accounts-page.module.ts
Normal file
@ -0,0 +1,25 @@
|
|||||||
|
import { CommonModule } from '@angular/common';
|
||||||
|
import { CUSTOM_ELEMENTS_SCHEMA, NgModule } from '@angular/core';
|
||||||
|
import { MatButtonModule } from '@angular/material/button';
|
||||||
|
import { RouterModule } from '@angular/router';
|
||||||
|
import { GfAccountsTableModule } from '@ghostfolio/client/components/accounts-table/accounts-table.module';
|
||||||
|
|
||||||
|
import { AccountsPageRoutingModule } from './accounts-page-routing.module';
|
||||||
|
import { AccountsPageComponent } from './accounts-page.component';
|
||||||
|
import { CreateOrUpdateAccountDialogModule } from './create-or-update-account-dialog/create-or-update-account-dialog.module';
|
||||||
|
|
||||||
|
@NgModule({
|
||||||
|
declarations: [AccountsPageComponent],
|
||||||
|
exports: [],
|
||||||
|
imports: [
|
||||||
|
AccountsPageRoutingModule,
|
||||||
|
CommonModule,
|
||||||
|
CreateOrUpdateAccountDialogModule,
|
||||||
|
GfAccountsTableModule,
|
||||||
|
MatButtonModule,
|
||||||
|
RouterModule
|
||||||
|
],
|
||||||
|
providers: [],
|
||||||
|
schemas: [CUSTOM_ELEMENTS_SCHEMA]
|
||||||
|
})
|
||||||
|
export class AccountsPageModule {}
|
8
apps/client/src/app/pages/accounts/accounts-page.scss
Normal file
8
apps/client/src/app/pages/accounts/accounts-page.scss
Normal file
@ -0,0 +1,8 @@
|
|||||||
|
:host {
|
||||||
|
.fab-container {
|
||||||
|
position: fixed;
|
||||||
|
right: 2rem;
|
||||||
|
bottom: 2rem;
|
||||||
|
z-index: 999;
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,104 @@
|
|||||||
|
import {
|
||||||
|
ChangeDetectionStrategy,
|
||||||
|
ChangeDetectorRef,
|
||||||
|
Component,
|
||||||
|
Inject
|
||||||
|
} from '@angular/core';
|
||||||
|
import { FormControl, Validators } from '@angular/forms';
|
||||||
|
import { MatAutocompleteSelectedEvent } from '@angular/material/autocomplete';
|
||||||
|
import { MAT_DIALOG_DATA, MatDialogRef } from '@angular/material/dialog';
|
||||||
|
import { LookupItem } from '@ghostfolio/api/app/symbol/interfaces/lookup-item.interface';
|
||||||
|
import { Currency } from '@prisma/client';
|
||||||
|
import { Observable, Subject } from 'rxjs';
|
||||||
|
import {
|
||||||
|
debounceTime,
|
||||||
|
distinctUntilChanged,
|
||||||
|
startWith,
|
||||||
|
switchMap,
|
||||||
|
takeUntil
|
||||||
|
} from 'rxjs/operators';
|
||||||
|
|
||||||
|
import { DataService } from '../../../services/data.service';
|
||||||
|
import { CreateOrUpdateAccountDialogParams } from './interfaces/interfaces';
|
||||||
|
|
||||||
|
@Component({
|
||||||
|
host: { class: 'h-100' },
|
||||||
|
selector: 'create-or-update-account-dialog',
|
||||||
|
changeDetection: ChangeDetectionStrategy.OnPush,
|
||||||
|
styleUrls: ['./create-or-update-account-dialog.scss'],
|
||||||
|
templateUrl: 'create-or-update-account-dialog.html'
|
||||||
|
})
|
||||||
|
export class CreateOrUpdateAccountDialog {
|
||||||
|
public currencies: Currency[] = [];
|
||||||
|
public filteredLookupItems: Observable<LookupItem[]>;
|
||||||
|
public isLoading = false;
|
||||||
|
public platforms: { id: string; name: string }[];
|
||||||
|
public searchSymbolCtrl = new FormControl(
|
||||||
|
this.data.transaction.symbol,
|
||||||
|
Validators.required
|
||||||
|
);
|
||||||
|
|
||||||
|
private unsubscribeSubject = new Subject<void>();
|
||||||
|
|
||||||
|
public constructor(
|
||||||
|
private cd: ChangeDetectorRef,
|
||||||
|
private dataService: DataService,
|
||||||
|
public dialogRef: MatDialogRef<CreateOrUpdateAccountDialog>,
|
||||||
|
@Inject(MAT_DIALOG_DATA) public data: CreateOrUpdateAccountDialogParams
|
||||||
|
) {}
|
||||||
|
|
||||||
|
ngOnInit() {
|
||||||
|
this.dataService.fetchInfo().subscribe(({ currencies, platforms }) => {
|
||||||
|
this.currencies = currencies;
|
||||||
|
this.platforms = platforms;
|
||||||
|
});
|
||||||
|
|
||||||
|
this.filteredLookupItems = this.searchSymbolCtrl.valueChanges.pipe(
|
||||||
|
startWith(''),
|
||||||
|
debounceTime(400),
|
||||||
|
distinctUntilChanged(),
|
||||||
|
switchMap((aQuery: string) => {
|
||||||
|
if (aQuery) {
|
||||||
|
return this.dataService.fetchSymbols(aQuery);
|
||||||
|
}
|
||||||
|
|
||||||
|
return [];
|
||||||
|
})
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
public onCancel(): void {
|
||||||
|
this.dialogRef.close();
|
||||||
|
}
|
||||||
|
|
||||||
|
public onUpdateSymbol(event: MatAutocompleteSelectedEvent) {
|
||||||
|
this.isLoading = true;
|
||||||
|
this.data.transaction.symbol = event.option.value;
|
||||||
|
|
||||||
|
this.dataService
|
||||||
|
.fetchSymbolItem(this.data.transaction.symbol)
|
||||||
|
.pipe(takeUntil(this.unsubscribeSubject))
|
||||||
|
.subscribe(({ currency, dataSource, marketPrice }) => {
|
||||||
|
this.data.transaction.currency = currency;
|
||||||
|
this.data.transaction.dataSource = dataSource;
|
||||||
|
this.data.transaction.unitPrice = marketPrice;
|
||||||
|
|
||||||
|
this.isLoading = false;
|
||||||
|
|
||||||
|
this.cd.markForCheck();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
public onUpdateSymbolByTyping(value: string) {
|
||||||
|
this.data.transaction.currency = null;
|
||||||
|
this.data.transaction.dataSource = null;
|
||||||
|
this.data.transaction.unitPrice = null;
|
||||||
|
|
||||||
|
this.data.transaction.symbol = value;
|
||||||
|
}
|
||||||
|
|
||||||
|
public ngOnDestroy() {
|
||||||
|
this.unsubscribeSubject.next();
|
||||||
|
this.unsubscribeSubject.complete();
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,163 @@
|
|||||||
|
<form #addAccountForm="ngForm" class="d-flex flex-column h-100">
|
||||||
|
<h1 *ngIf="data.transaction.id" mat-dialog-title i18n>Update account</h1>
|
||||||
|
<h1 *ngIf="!data.transaction.id" mat-dialog-title i18n>Add account</h1>
|
||||||
|
<div class="flex-grow-1" mat-dialog-content>
|
||||||
|
<div>
|
||||||
|
<mat-form-field appearance="outline" class="w-100">
|
||||||
|
<mat-label i18n>Symbol or ISIN</mat-label>
|
||||||
|
<input
|
||||||
|
autocapitalize="off"
|
||||||
|
autocomplete="off"
|
||||||
|
autocorrect="off"
|
||||||
|
matInput
|
||||||
|
required
|
||||||
|
[formControl]="searchSymbolCtrl"
|
||||||
|
[matAutocomplete]="auto"
|
||||||
|
(change)="onUpdateSymbolByTyping($event.target.value)"
|
||||||
|
/>
|
||||||
|
<mat-autocomplete
|
||||||
|
#auto="matAutocomplete"
|
||||||
|
(optionSelected)="onUpdateSymbol($event)"
|
||||||
|
>
|
||||||
|
<ng-container>
|
||||||
|
<mat-option
|
||||||
|
*ngFor="let lookupItem of filteredLookupItems | async"
|
||||||
|
class="autocomplete"
|
||||||
|
[value]="lookupItem.symbol"
|
||||||
|
>
|
||||||
|
<span class="mr-2 symbol">{{ lookupItem.symbol | gfSymbol }}</span
|
||||||
|
><span><b>{{ lookupItem.name }}</b></span>
|
||||||
|
</mat-option>
|
||||||
|
</ng-container>
|
||||||
|
</mat-autocomplete>
|
||||||
|
<mat-spinner *ngIf="isLoading" matSuffix [diameter]="20"></mat-spinner>
|
||||||
|
</mat-form-field>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<mat-form-field appearance="outline" class="w-100">
|
||||||
|
<mat-label i18n>Type</mat-label>
|
||||||
|
<mat-select name="type" required [(value)]="data.transaction.type">
|
||||||
|
<mat-option value="BUY" i18n> BUY </mat-option>
|
||||||
|
<mat-option value="SELL" i18n> SELL </mat-option>
|
||||||
|
</mat-select>
|
||||||
|
</mat-form-field>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<mat-form-field appearance="outline" class="w-100">
|
||||||
|
<mat-label i18n>Currency</mat-label>
|
||||||
|
<mat-select
|
||||||
|
class="no-arrow"
|
||||||
|
disabled
|
||||||
|
name="currency"
|
||||||
|
required
|
||||||
|
[(value)]="data.transaction.currency"
|
||||||
|
>
|
||||||
|
<mat-option *ngFor="let currency of currencies" [value]="currency"
|
||||||
|
>{{ currency }}</mat-option
|
||||||
|
>
|
||||||
|
</mat-select>
|
||||||
|
</mat-form-field>
|
||||||
|
</div>
|
||||||
|
<div class="d-none">
|
||||||
|
<mat-form-field appearance="outline" class="w-100">
|
||||||
|
<mat-label i18n>Data Source</mat-label>
|
||||||
|
<input
|
||||||
|
disabled
|
||||||
|
matInput
|
||||||
|
name="dataSource"
|
||||||
|
required
|
||||||
|
[(ngModel)]="data.transaction.dataSource"
|
||||||
|
/>
|
||||||
|
</mat-form-field>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<mat-form-field appearance="outline" class="w-100">
|
||||||
|
<mat-label i18n>Date</mat-label>
|
||||||
|
<input
|
||||||
|
disabled
|
||||||
|
matInput
|
||||||
|
name="date"
|
||||||
|
required
|
||||||
|
[matDatepicker]="date"
|
||||||
|
[(ngModel)]="data.transaction.date"
|
||||||
|
/>
|
||||||
|
<mat-datepicker-toggle matSuffix [for]="date"></mat-datepicker-toggle>
|
||||||
|
<mat-datepicker #date disabled="false"></mat-datepicker>
|
||||||
|
</mat-form-field>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<mat-form-field appearance="outline" class="w-100">
|
||||||
|
<mat-label i18n>Fee</mat-label>
|
||||||
|
<input
|
||||||
|
matInput
|
||||||
|
name="fee"
|
||||||
|
required
|
||||||
|
type="number"
|
||||||
|
[(ngModel)]="data.transaction.fee"
|
||||||
|
/>
|
||||||
|
</mat-form-field>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<mat-form-field appearance="outline" class="w-100">
|
||||||
|
<mat-label i18n>Quantity</mat-label>
|
||||||
|
<input
|
||||||
|
matInput
|
||||||
|
name="quantity"
|
||||||
|
required
|
||||||
|
type="number"
|
||||||
|
[(ngModel)]="data.transaction.quantity"
|
||||||
|
/>
|
||||||
|
</mat-form-field>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<mat-form-field appearance="outline" class="w-100">
|
||||||
|
<mat-label i18n>Unit Price</mat-label>
|
||||||
|
<input
|
||||||
|
matInput
|
||||||
|
name="unitPrice"
|
||||||
|
required
|
||||||
|
type="number"
|
||||||
|
[(ngModel)]="data.transaction.unitPrice"
|
||||||
|
/>
|
||||||
|
</mat-form-field>
|
||||||
|
</div>
|
||||||
|
<div class="d-none">
|
||||||
|
<mat-form-field appearance="outline" class="w-100">
|
||||||
|
<mat-label i18n>Account</mat-label>
|
||||||
|
<mat-select
|
||||||
|
disabled
|
||||||
|
name="accountId"
|
||||||
|
required
|
||||||
|
[(value)]="data.transaction.accountId"
|
||||||
|
>
|
||||||
|
<mat-option *ngFor="let account of data.accounts" [value]="account.id"
|
||||||
|
>{{ account.name }}</mat-option
|
||||||
|
>
|
||||||
|
</mat-select>
|
||||||
|
</mat-form-field>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<mat-form-field appearance="outline" class="w-100">
|
||||||
|
<mat-label i18n>Platform</mat-label>
|
||||||
|
<mat-select name="platformId" [(value)]="data.transaction.platformId">
|
||||||
|
<mat-option [value]="null"></mat-option>
|
||||||
|
<mat-option *ngFor="let platform of platforms" [value]="platform.id"
|
||||||
|
>{{ platform.name }}</mat-option
|
||||||
|
>
|
||||||
|
</mat-select>
|
||||||
|
</mat-form-field>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="justify-content-end" mat-dialog-actions>
|
||||||
|
<button i18n mat-button (click)="onCancel()">Cancel</button>
|
||||||
|
<button
|
||||||
|
color="primary"
|
||||||
|
i18n
|
||||||
|
mat-flat-button
|
||||||
|
[disabled]="!(addAccountForm.form.valid && data.transaction.symbol)"
|
||||||
|
[mat-dialog-close]="data"
|
||||||
|
>
|
||||||
|
Save
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</form>
|
@ -0,0 +1,35 @@
|
|||||||
|
import { CommonModule } from '@angular/common';
|
||||||
|
import { NgModule } from '@angular/core';
|
||||||
|
import { FormsModule, ReactiveFormsModule } from '@angular/forms';
|
||||||
|
import { MatAutocompleteModule } from '@angular/material/autocomplete';
|
||||||
|
import { MatButtonModule } from '@angular/material/button';
|
||||||
|
import { MatDatepickerModule } from '@angular/material/datepicker';
|
||||||
|
import { MatDialogModule } from '@angular/material/dialog';
|
||||||
|
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 { CreateOrUpdateAccountDialog } from './create-or-update-account-dialog.component';
|
||||||
|
|
||||||
|
@NgModule({
|
||||||
|
declarations: [CreateOrUpdateAccountDialog],
|
||||||
|
exports: [],
|
||||||
|
imports: [
|
||||||
|
CommonModule,
|
||||||
|
GfSymbolModule,
|
||||||
|
FormsModule,
|
||||||
|
MatAutocompleteModule,
|
||||||
|
MatButtonModule,
|
||||||
|
MatDatepickerModule,
|
||||||
|
MatDialogModule,
|
||||||
|
MatFormFieldModule,
|
||||||
|
MatInputModule,
|
||||||
|
MatProgressSpinnerModule,
|
||||||
|
MatSelectModule,
|
||||||
|
ReactiveFormsModule
|
||||||
|
],
|
||||||
|
providers: []
|
||||||
|
})
|
||||||
|
export class CreateOrUpdateAccountDialogModule {}
|
@ -0,0 +1,43 @@
|
|||||||
|
:host {
|
||||||
|
display: block;
|
||||||
|
|
||||||
|
.mat-dialog-content {
|
||||||
|
max-height: unset;
|
||||||
|
|
||||||
|
.autocomplete {
|
||||||
|
font-size: 90%;
|
||||||
|
height: 2.5rem;
|
||||||
|
|
||||||
|
.symbol {
|
||||||
|
display: inline-block;
|
||||||
|
min-width: 4rem;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.mat-select {
|
||||||
|
&.no-arrow {
|
||||||
|
::ng-deep {
|
||||||
|
.mat-select-arrow {
|
||||||
|
opacity: 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.mat-datepicker-input {
|
||||||
|
&.mat-input-element:disabled {
|
||||||
|
color: var(--dark-primary-text);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
:host-context(.is-dark-theme) {
|
||||||
|
.mat-dialog-content {
|
||||||
|
.mat-datepicker-input {
|
||||||
|
&.mat-input-element:disabled {
|
||||||
|
color: var(--light-primary-text);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,9 @@
|
|||||||
|
import { Account } from '@prisma/client';
|
||||||
|
|
||||||
|
import { Order } from '../../interfaces/order.interface';
|
||||||
|
|
||||||
|
export interface CreateOrUpdateAccountDialogParams {
|
||||||
|
accountId: string;
|
||||||
|
accounts: Account[];
|
||||||
|
transaction: Order;
|
||||||
|
}
|
@ -0,0 +1,15 @@
|
|||||||
|
import { Currency, DataSource } from '@prisma/client';
|
||||||
|
|
||||||
|
export interface Order {
|
||||||
|
accountId: string;
|
||||||
|
currency: Currency;
|
||||||
|
dataSource: DataSource;
|
||||||
|
date: Date;
|
||||||
|
fee: number;
|
||||||
|
id: string;
|
||||||
|
quantity: number;
|
||||||
|
platformId: string;
|
||||||
|
symbol: string;
|
||||||
|
type: string;
|
||||||
|
unitPrice: number;
|
||||||
|
}
|
@ -1,4 +1,4 @@
|
|||||||
import { Currency, DataSource } from '.prisma/client';
|
import { Currency, DataSource } from '@prisma/client';
|
||||||
|
|
||||||
export interface Order {
|
export interface Order {
|
||||||
accountId: string;
|
accountId: string;
|
||||||
|
@ -1,6 +1,7 @@
|
|||||||
import { HttpClient } from '@angular/common/http';
|
import { HttpClient } from '@angular/common/http';
|
||||||
import { Injectable } from '@angular/core';
|
import { Injectable } from '@angular/core';
|
||||||
import { Access } from '@ghostfolio/api/app/access/interfaces/access.interface';
|
import { Access } from '@ghostfolio/api/app/access/interfaces/access.interface';
|
||||||
|
import { UpdateAccountDto } from '@ghostfolio/api/app/account/update-account.dto';
|
||||||
import { AdminData } from '@ghostfolio/api/app/admin/interfaces/admin-data.interface';
|
import { AdminData } from '@ghostfolio/api/app/admin/interfaces/admin-data.interface';
|
||||||
import { InfoItem } from '@ghostfolio/api/app/info/interfaces/info-item.interface';
|
import { InfoItem } from '@ghostfolio/api/app/info/interfaces/info-item.interface';
|
||||||
import { UpdateOrderDto } from '@ghostfolio/api/app/order/update-order.dto';
|
import { UpdateOrderDto } from '@ghostfolio/api/app/order/update-order.dto';
|
||||||
@ -28,10 +29,18 @@ export class DataService {
|
|||||||
|
|
||||||
public constructor(private http: HttpClient) {}
|
public constructor(private http: HttpClient) {}
|
||||||
|
|
||||||
|
public fetchAccounts() {
|
||||||
|
return this.http.get<OrderModel[]>('/api/account');
|
||||||
|
}
|
||||||
|
|
||||||
public fetchAdminData() {
|
public fetchAdminData() {
|
||||||
return this.http.get<AdminData>('/api/admin');
|
return this.http.get<AdminData>('/api/admin');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public deleteAccount(aId: string) {
|
||||||
|
return this.http.delete<any>(`/api/account/${aId}`);
|
||||||
|
}
|
||||||
|
|
||||||
public deleteOrder(aId: string) {
|
public deleteOrder(aId: string) {
|
||||||
return this.http.delete<any>(`/api/order/${aId}`);
|
return this.http.delete<any>(`/api/order/${aId}`);
|
||||||
}
|
}
|
||||||
@ -108,6 +117,10 @@ export class DataService {
|
|||||||
return this.http.get<any>(`/api/auth/anonymous/${accessToken}`);
|
return this.http.get<any>(`/api/auth/anonymous/${accessToken}`);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public postAccount(aAccount: UpdateAccountDto) {
|
||||||
|
return this.http.post<OrderModel>(`/api/account`, aAccount);
|
||||||
|
}
|
||||||
|
|
||||||
public postOrder(aOrder: UpdateOrderDto) {
|
public postOrder(aOrder: UpdateOrderDto) {
|
||||||
return this.http.post<OrderModel>(`/api/order`, aOrder);
|
return this.http.post<OrderModel>(`/api/order`, aOrder);
|
||||||
}
|
}
|
||||||
@ -116,6 +129,10 @@ export class DataService {
|
|||||||
return this.http.post<UserItem>(`/api/user`, {});
|
return this.http.post<UserItem>(`/api/user`, {});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public putAccount(aAccount: UpdateAccountDto) {
|
||||||
|
return this.http.put<UserItem>(`/api/account/${aAccount.id}`, aAccount);
|
||||||
|
}
|
||||||
|
|
||||||
public putOrder(aOrder: UpdateOrderDto) {
|
public putOrder(aOrder: UpdateOrderDto) {
|
||||||
return this.http.put<UserItem>(`/api/order/${aOrder.id}`, aOrder);
|
return this.http.put<UserItem>(`/api/order/${aOrder.id}`, aOrder);
|
||||||
}
|
}
|
||||||
|
@ -9,10 +9,13 @@ export const permissions = {
|
|||||||
accessFearAndGreedIndex: 'accessFearAndGreedIndex',
|
accessFearAndGreedIndex: 'accessFearAndGreedIndex',
|
||||||
createAccount: 'createAccount',
|
createAccount: 'createAccount',
|
||||||
createOrder: 'createOrder',
|
createOrder: 'createOrder',
|
||||||
|
createUserAccount: 'createUserAccount',
|
||||||
|
deleteAccount: 'deleteAcccount',
|
||||||
deleteOrder: 'deleteOrder',
|
deleteOrder: 'deleteOrder',
|
||||||
enableSocialLogin: 'enableSocialLogin',
|
enableSocialLogin: 'enableSocialLogin',
|
||||||
enableSubscription: 'enableSubscription',
|
enableSubscription: 'enableSubscription',
|
||||||
readForeignPortfolio: 'readForeignPortfolio',
|
readForeignPortfolio: 'readForeignPortfolio',
|
||||||
|
updateAccount: 'updateAccount',
|
||||||
updateOrder: 'updateOrder',
|
updateOrder: 'updateOrder',
|
||||||
updateUserSettings: 'updateUserSettings'
|
updateUserSettings: 'updateUserSettings'
|
||||||
};
|
};
|
||||||
@ -37,7 +40,7 @@ export function getPermissions(aRole: Role): string[] {
|
|||||||
];
|
];
|
||||||
|
|
||||||
case 'DEMO':
|
case 'DEMO':
|
||||||
return [permissions.createAccount];
|
return [permissions.createUserAccount];
|
||||||
|
|
||||||
case 'USER':
|
case 'USER':
|
||||||
return [
|
return [
|
||||||
|
Loading…
x
Reference in New Issue
Block a user