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