mirror of https://github.com/ghostfolio/ghostfolio
31 changed files with 1463 additions and 12 deletions
@ -0,0 +1,250 @@ |
|||||
|
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 { Order as OrderModel } from '@prisma/client'; |
||||
|
import { parseISO } from 'date-fns'; |
||||
|
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 deleteOrder(@Param('id') id: string): Promise<OrderModel> { |
||||
|
if ( |
||||
|
!hasPermission( |
||||
|
getPermissions(this.request.user.role), |
||||
|
permissions.deleteOrder |
||||
|
) |
||||
|
) { |
||||
|
throw new HttpException( |
||||
|
getReasonPhrase(StatusCodes.FORBIDDEN), |
||||
|
StatusCodes.FORBIDDEN |
||||
|
); |
||||
|
} |
||||
|
|
||||
|
return this.accountService.deleteOrder( |
||||
|
{ |
||||
|
id_userId: { |
||||
|
id, |
||||
|
userId: this.request.user.id |
||||
|
} |
||||
|
}, |
||||
|
this.request.user.id |
||||
|
); |
||||
|
} |
||||
|
|
||||
|
@Get() |
||||
|
@UseGuards(AuthGuard('jwt')) |
||||
|
public async getAllOrders( |
||||
|
@Headers('impersonation-id') impersonationId |
||||
|
): Promise<OrderModel[]> { |
||||
|
const impersonationUserId = await this.impersonationService.validateImpersonationId( |
||||
|
impersonationId, |
||||
|
this.request.user.id |
||||
|
); |
||||
|
|
||||
|
let orders = await this.accountService.orders({ |
||||
|
include: { |
||||
|
Account: { |
||||
|
include: { |
||||
|
Platform: true |
||||
|
} |
||||
|
} |
||||
|
}, |
||||
|
orderBy: { date: 'desc' }, |
||||
|
where: { userId: impersonationUserId || this.request.user.id } |
||||
|
}); |
||||
|
|
||||
|
if ( |
||||
|
impersonationUserId && |
||||
|
!hasPermission( |
||||
|
getPermissions(this.request.user.role), |
||||
|
permissions.readForeignPortfolio |
||||
|
) |
||||
|
) { |
||||
|
orders = nullifyValuesInObjects(orders, ['fee', 'quantity', 'unitPrice']); |
||||
|
} |
||||
|
|
||||
|
return orders; |
||||
|
} |
||||
|
|
||||
|
@Get(':id') |
||||
|
@UseGuards(AuthGuard('jwt')) |
||||
|
public async getOrderById(@Param('id') id: string): Promise<OrderModel> { |
||||
|
return this.accountService.order({ |
||||
|
id_userId: { |
||||
|
id, |
||||
|
userId: this.request.user.id |
||||
|
} |
||||
|
}); |
||||
|
} |
||||
|
|
||||
|
@Post() |
||||
|
@UseGuards(AuthGuard('jwt')) |
||||
|
public async createOrder( |
||||
|
@Body() data: CreateAccountDto |
||||
|
): Promise<OrderModel> { |
||||
|
if ( |
||||
|
!hasPermission( |
||||
|
getPermissions(this.request.user.role), |
||||
|
permissions.createOrder |
||||
|
) |
||||
|
) { |
||||
|
throw new HttpException( |
||||
|
getReasonPhrase(StatusCodes.FORBIDDEN), |
||||
|
StatusCodes.FORBIDDEN |
||||
|
); |
||||
|
} |
||||
|
|
||||
|
const date = parseISO(data.date); |
||||
|
|
||||
|
const accountId = data.accountId; |
||||
|
delete data.accountId; |
||||
|
|
||||
|
if (data.platformId) { |
||||
|
const platformId = data.platformId; |
||||
|
delete data.platformId; |
||||
|
|
||||
|
return this.accountService.createOrder( |
||||
|
{ |
||||
|
...data, |
||||
|
date, |
||||
|
Account: { |
||||
|
connect: { |
||||
|
id_userId: { id: accountId, userId: this.request.user.id } |
||||
|
} |
||||
|
}, |
||||
|
Platform: { connect: { id: platformId } }, |
||||
|
User: { connect: { id: this.request.user.id } } |
||||
|
}, |
||||
|
this.request.user.id |
||||
|
); |
||||
|
} else { |
||||
|
delete data.platformId; |
||||
|
|
||||
|
return this.accountService.createOrder( |
||||
|
{ |
||||
|
...data, |
||||
|
date, |
||||
|
Account: { |
||||
|
connect: { |
||||
|
id_userId: { id: accountId, userId: this.request.user.id } |
||||
|
} |
||||
|
}, |
||||
|
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.updateOrder |
||||
|
) |
||||
|
) { |
||||
|
throw new HttpException( |
||||
|
getReasonPhrase(StatusCodes.FORBIDDEN), |
||||
|
StatusCodes.FORBIDDEN |
||||
|
); |
||||
|
} |
||||
|
|
||||
|
const originalOrder = await this.accountService.order({ |
||||
|
id_userId: { |
||||
|
id, |
||||
|
userId: this.request.user.id |
||||
|
} |
||||
|
}); |
||||
|
|
||||
|
const date = parseISO(data.date); |
||||
|
|
||||
|
const accountId = data.accountId; |
||||
|
delete data.accountId; |
||||
|
|
||||
|
if (data.platformId) { |
||||
|
const platformId = data.platformId; |
||||
|
delete data.platformId; |
||||
|
|
||||
|
return this.accountService.updateOrder( |
||||
|
{ |
||||
|
data: { |
||||
|
...data, |
||||
|
date, |
||||
|
Account: { |
||||
|
connect: { |
||||
|
id_userId: { id: accountId, userId: this.request.user.id } |
||||
|
} |
||||
|
}, |
||||
|
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.updateOrder( |
||||
|
{ |
||||
|
data: { |
||||
|
...data, |
||||
|
date, |
||||
|
Account: { |
||||
|
connect: { |
||||
|
id_userId: { id: accountId, userId: this.request.user.id } |
||||
|
} |
||||
|
}, |
||||
|
Platform: originalOrder.platformId |
||||
|
? { disconnect: true } |
||||
|
: undefined, |
||||
|
User: { connect: { id: this.request.user.id } } |
||||
|
}, |
||||
|
where: { |
||||
|
id_userId: { |
||||
|
id, |
||||
|
userId: this.request.user.id |
||||
|
} |
||||
|
} |
||||
|
}, |
||||
|
this.request.user.id |
||||
|
); |
||||
|
} |
||||
|
} |
||||
|
} |
@ -0,0 +1,34 @@ |
|||||
|
import { ConfigurationService } from '@ghostfolio/api/services/configuration.service'; |
||||
|
import { DataGatheringService } from '@ghostfolio/api/services/data-gathering.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 { CacheService } from '../cache/cache.service'; |
||||
|
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, |
||||
|
CacheService, |
||||
|
ConfigurationService, |
||||
|
DataGatheringService, |
||||
|
DataProviderService, |
||||
|
GhostfolioScraperApiService, |
||||
|
ImpersonationService, |
||||
|
PrismaService, |
||||
|
RakutenRapidApiService, |
||||
|
YahooFinanceService |
||||
|
] |
||||
|
}) |
||||
|
export class AccountModule {} |
@ -0,0 +1,105 @@ |
|||||
|
import { DataGatheringService } from '@ghostfolio/api/services/data-gathering.service'; |
||||
|
import { PrismaService } from '@ghostfolio/api/services/prisma.service'; |
||||
|
import { Injectable } from '@nestjs/common'; |
||||
|
import { Order, Prisma } from '@prisma/client'; |
||||
|
|
||||
|
import { CacheService } from '../cache/cache.service'; |
||||
|
import { RedisCacheService } from '../redis-cache/redis-cache.service'; |
||||
|
import { OrderWithPlatform } from './interfaces/order-with-platform.type'; |
||||
|
|
||||
|
@Injectable() |
||||
|
export class AccountService { |
||||
|
public constructor( |
||||
|
private readonly cacheService: CacheService, |
||||
|
private readonly dataGatheringService: DataGatheringService, |
||||
|
private readonly redisCacheService: RedisCacheService, |
||||
|
private prisma: PrismaService |
||||
|
) {} |
||||
|
|
||||
|
public async order( |
||||
|
orderWhereUniqueInput: Prisma.OrderWhereUniqueInput |
||||
|
): Promise<Order | null> { |
||||
|
return this.prisma.order.findUnique({ |
||||
|
where: orderWhereUniqueInput |
||||
|
}); |
||||
|
} |
||||
|
|
||||
|
public async orders(params: { |
||||
|
include?: Prisma.OrderInclude; |
||||
|
skip?: number; |
||||
|
take?: number; |
||||
|
cursor?: Prisma.OrderWhereUniqueInput; |
||||
|
where?: Prisma.OrderWhereInput; |
||||
|
orderBy?: Prisma.OrderOrderByInput; |
||||
|
}): Promise<OrderWithPlatform[]> { |
||||
|
const { include, skip, take, cursor, where, orderBy } = params; |
||||
|
|
||||
|
return this.prisma.order.findMany({ |
||||
|
cursor, |
||||
|
include, |
||||
|
orderBy, |
||||
|
skip, |
||||
|
take, |
||||
|
where |
||||
|
}); |
||||
|
} |
||||
|
|
||||
|
public async createOrder( |
||||
|
data: Prisma.OrderCreateInput, |
||||
|
aUserId: string |
||||
|
): Promise<Order> { |
||||
|
this.redisCacheService.remove(`${aUserId}.portfolio`); |
||||
|
|
||||
|
// Gather symbol data of order in the background
|
||||
|
this.dataGatheringService.gatherSymbols([ |
||||
|
{ |
||||
|
date: <Date>data.date, |
||||
|
symbol: data.symbol |
||||
|
} |
||||
|
]); |
||||
|
|
||||
|
await this.cacheService.flush(aUserId); |
||||
|
|
||||
|
return this.prisma.order.create({ |
||||
|
data |
||||
|
}); |
||||
|
} |
||||
|
|
||||
|
public async deleteOrder( |
||||
|
where: Prisma.OrderWhereUniqueInput, |
||||
|
aUserId: string |
||||
|
): Promise<Order> { |
||||
|
this.redisCacheService.remove(`${aUserId}.portfolio`); |
||||
|
|
||||
|
return this.prisma.order.delete({ |
||||
|
where |
||||
|
}); |
||||
|
} |
||||
|
|
||||
|
public async updateOrder( |
||||
|
params: { |
||||
|
where: Prisma.OrderWhereUniqueInput; |
||||
|
data: Prisma.OrderUpdateInput; |
||||
|
}, |
||||
|
aUserId: string |
||||
|
): Promise<Order> { |
||||
|
const { data, where } = params; |
||||
|
|
||||
|
this.redisCacheService.remove(`${aUserId}.portfolio`); |
||||
|
|
||||
|
// Gather symbol data of order in the background
|
||||
|
this.dataGatheringService.gatherSymbols([ |
||||
|
{ |
||||
|
date: <Date>data.date, |
||||
|
symbol: <string>data.symbol |
||||
|
} |
||||
|
]); |
||||
|
|
||||
|
await this.cacheService.flush(aUserId); |
||||
|
|
||||
|
return this.prisma.order.update({ |
||||
|
data, |
||||
|
where |
||||
|
}); |
||||
|
} |
||||
|
} |
@ -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 }; |
@ -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; |
||||
|
} |
@ -0,0 +1,91 @@ |
|||||
|
<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> |
||||
|
<div class="d-flex"> |
||||
|
<gf-symbol-icon |
||||
|
*ngIf="element.Account?.Platform?.url" |
||||
|
class="mr-2" |
||||
|
[tooltip]="element.Account?.Platform?.name" |
||||
|
[url]="element.Account?.Platform?.url" |
||||
|
></gf-symbol-icon> |
||||
|
<span class="d-none d-lg-block">{{ element.Account?.name }}</span> |
||||
|
</div> |
||||
|
</td> |
||||
|
</ng-container> |
||||
|
|
||||
|
<ng-container matColumnDef="type"> |
||||
|
<th |
||||
|
*matHeaderCellDef |
||||
|
class="justify-content-center" |
||||
|
i18n |
||||
|
mat-header-cell |
||||
|
mat-sort-header |
||||
|
> |
||||
|
Type |
||||
|
</th> |
||||
|
<td mat-cell *matCellDef="let element" class="text-center"> |
||||
|
<div |
||||
|
class="d-inline-flex justify-content-center pl-1 pr-2 py-1 type-badge" |
||||
|
[ngClass]="element.type == 'BUY' ? 'buy' : 'sell'" |
||||
|
> |
||||
|
<ion-icon |
||||
|
class="mr-1" |
||||
|
[name]=" |
||||
|
element.type === 'BUY' |
||||
|
? 'arrow-forward-circle-outline' |
||||
|
: 'arrow-back-circle-outline' |
||||
|
" |
||||
|
></ion-icon> |
||||
|
<span>{{ element.type }}</span> |
||||
|
</div> |
||||
|
</td> |
||||
|
</ng-container> |
||||
|
|
||||
|
<ng-container matColumnDef="symbol"> |
||||
|
<th *matHeaderCellDef i18n mat-header-cell mat-sort-header>Platform</th> |
||||
|
<td mat-cell *matCellDef="let element">{{ element.symbol }}</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)="onUpdateTransaction(element)"> |
||||
|
Edit |
||||
|
</button> |
||||
|
<button i18n mat-menu-item (click)="onDeleteTransaction(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,71 @@ |
|||||
|
: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; |
||||
|
|
||||
|
ion-icon { |
||||
|
font-size: 1rem; |
||||
|
} |
||||
|
|
||||
|
&.buy { |
||||
|
color: var(--green); |
||||
|
} |
||||
|
|
||||
|
&.sell { |
||||
|
color: var(--orange); |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
: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,87 @@ |
|||||
|
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() transactionDeleted = new EventEmitter<string>(); |
||||
|
@Output() transactionToUpdate = 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', 'symbol']; |
||||
|
|
||||
|
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 onDeleteTransaction(aId: string) { |
||||
|
const confirmation = confirm( |
||||
|
'Do you really want to delete this transaction?' |
||||
|
); |
||||
|
|
||||
|
if (confirmation) { |
||||
|
this.transactionDeleted.emit(aId); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
public onUpdateTransaction(aTransaction: OrderModel) { |
||||
|
this.transactionToUpdate.emit(aTransaction); |
||||
|
} |
||||
|
|
||||
|
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 {} |
@ -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 {} |
@ -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 hasPermissionToCreateOrder: boolean; |
||||
|
public hasPermissionToDeleteOrder: 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.openCreateTransactionDialog(); |
||||
|
} else if (params['editDialog']) { |
||||
|
if (this.accounts) { |
||||
|
const transaction = this.accounts.find((transaction) => { |
||||
|
return transaction.id === params['transactionId']; |
||||
|
}); |
||||
|
|
||||
|
this.openUpdateTransactionDialog(transaction); |
||||
|
} 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.hasPermissionToCreateOrder = hasPermission( |
||||
|
user.permissions, |
||||
|
permissions.createOrder |
||||
|
); |
||||
|
this.hasPermissionToDeleteOrder = hasPermission( |
||||
|
user.permissions, |
||||
|
permissions.deleteOrder |
||||
|
); |
||||
|
|
||||
|
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 onDeleteTransaction(aId: string) { |
||||
|
this.dataService.deleteAccount(aId).subscribe({ |
||||
|
next: () => { |
||||
|
this.fetchAccounts(); |
||||
|
} |
||||
|
}); |
||||
|
} |
||||
|
|
||||
|
public onUpdateTransaction(aTransaction: OrderModel) { |
||||
|
this.router.navigate([], { |
||||
|
queryParams: { editDialog: true, transactionId: aTransaction.id } |
||||
|
}); |
||||
|
} |
||||
|
|
||||
|
public openUpdateTransactionDialog({ |
||||
|
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 openCreateTransactionDialog(): 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 }); |
||||
|
}); |
||||
|
} |
||||
|
} |
@ -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 && hasPermissionToDeleteOrder" |
||||
|
(transactionDeleted)="onDeleteTransaction($event)" |
||||
|
(transactionToUpdate)="onUpdateTransaction($event)" |
||||
|
></gf-accounts-table> |
||||
|
</div> |
||||
|
</div> |
||||
|
|
||||
|
<div |
||||
|
*ngIf="!hasImpersonationId && hasPermissionToCreateOrder" |
||||
|
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> |
@ -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 {} |
@ -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 #addTransactionForm="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]="!(addTransactionForm.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; |
||||
|
} |
Loading…
Reference in new issue