mirror of https://github.com/ghostfolio/ghostfolio
committed by
GitHub
27 changed files with 438 additions and 57 deletions
@ -0,0 +1,11 @@ |
|||||
|
import { PrismaModule } from '@ghostfolio/api/services/prisma.module'; |
||||
|
import { Module } from '@nestjs/common'; |
||||
|
|
||||
|
import { MarketDataService } from './market-data.service'; |
||||
|
|
||||
|
@Module({ |
||||
|
exports: [MarketDataService], |
||||
|
imports: [PrismaModule], |
||||
|
providers: [MarketDataService] |
||||
|
}) |
||||
|
export class MarketDataModule {} |
@ -0,0 +1,18 @@ |
|||||
|
<div> |
||||
|
<div *ngFor="let itemByMonth of marketDataByMonth | keyvalue" class="d-flex"> |
||||
|
<div>{{ itemByMonth.key }}</div> |
||||
|
<div class="align-items-center d-flex flex-grow-1 justify-content-end"> |
||||
|
<div |
||||
|
*ngFor="let dayItem of days; let i = index" |
||||
|
class="day" |
||||
|
[title]=" |
||||
|
(marketDataByMonth[itemByMonth.key][i + 1]?.date |
||||
|
| date: defaultDateFormat) ?? '' |
||||
|
" |
||||
|
[ngClass]="{ |
||||
|
available: marketDataByMonth[itemByMonth.key][i + 1]?.day == i + 1 |
||||
|
}" |
||||
|
></div> |
||||
|
</div> |
||||
|
</div> |
||||
|
</div> |
@ -0,0 +1,16 @@ |
|||||
|
@import '~apps/client/src/styles/ghostfolio-style'; |
||||
|
|
||||
|
:host { |
||||
|
display: block; |
||||
|
|
||||
|
.day { |
||||
|
background-color: var(--danger); |
||||
|
height: 0.5rem; |
||||
|
margin-right: 0.25rem; |
||||
|
width: 0.5rem; |
||||
|
|
||||
|
&.available { |
||||
|
background-color: var(--success); |
||||
|
} |
||||
|
} |
||||
|
} |
@ -0,0 +1,48 @@ |
|||||
|
import { |
||||
|
ChangeDetectionStrategy, |
||||
|
Component, |
||||
|
Input, |
||||
|
OnChanges, |
||||
|
OnInit |
||||
|
} from '@angular/core'; |
||||
|
import { DEFAULT_DATE_FORMAT } from '@ghostfolio/common/config'; |
||||
|
import { MarketData } from '@prisma/client'; |
||||
|
import { format } from 'date-fns'; |
||||
|
|
||||
|
@Component({ |
||||
|
changeDetection: ChangeDetectionStrategy.OnPush, |
||||
|
selector: 'gf-admin-market-data-detail', |
||||
|
styleUrls: ['./admin-market-data-detail.component.scss'], |
||||
|
templateUrl: './admin-market-data-detail.component.html' |
||||
|
}) |
||||
|
export class AdminMarketDataDetailComponent implements OnChanges, OnInit { |
||||
|
@Input() marketData: MarketData[]; |
||||
|
|
||||
|
public days = Array(31); |
||||
|
public defaultDateFormat = DEFAULT_DATE_FORMAT; |
||||
|
public marketDataByMonth: { |
||||
|
[yearMonth: string]: { [day: string]: MarketData & { day: number } }; |
||||
|
} = {}; |
||||
|
|
||||
|
public constructor() {} |
||||
|
|
||||
|
public ngOnInit() {} |
||||
|
|
||||
|
public ngOnChanges() { |
||||
|
this.marketDataByMonth = {}; |
||||
|
|
||||
|
for (const marketDataItem of this.marketData) { |
||||
|
const currentDay = parseInt(format(marketDataItem.date, 'd'), 10); |
||||
|
const key = format(marketDataItem.date, 'yyyy-MM'); |
||||
|
|
||||
|
if (!this.marketDataByMonth[key]) { |
||||
|
this.marketDataByMonth[key] = {}; |
||||
|
} |
||||
|
|
||||
|
this.marketDataByMonth[key][currentDay] = { |
||||
|
...marketDataItem, |
||||
|
day: currentDay |
||||
|
}; |
||||
|
} |
||||
|
} |
||||
|
} |
@ -0,0 +1,13 @@ |
|||||
|
import { CommonModule } from '@angular/common'; |
||||
|
import { CUSTOM_ELEMENTS_SCHEMA, NgModule } from '@angular/core'; |
||||
|
|
||||
|
import { AdminMarketDataDetailComponent } from './admin-market-data-detail.component'; |
||||
|
|
||||
|
@NgModule({ |
||||
|
declarations: [AdminMarketDataDetailComponent], |
||||
|
exports: [AdminMarketDataDetailComponent], |
||||
|
imports: [CommonModule], |
||||
|
providers: [], |
||||
|
schemas: [CUSTOM_ELEMENTS_SCHEMA] |
||||
|
}) |
||||
|
export class GfAdminMarketDataDetailModule {} |
@ -0,0 +1,82 @@ |
|||||
|
import { |
||||
|
ChangeDetectionStrategy, |
||||
|
ChangeDetectorRef, |
||||
|
Component, |
||||
|
OnDestroy, |
||||
|
OnInit |
||||
|
} from '@angular/core'; |
||||
|
import { DataService } from '@ghostfolio/client/services/data.service'; |
||||
|
import { DEFAULT_DATE_FORMAT } from '@ghostfolio/common/config'; |
||||
|
import { AdminMarketDataItem } from '@ghostfolio/common/interfaces/admin-market-data.interface'; |
||||
|
import { MarketData } from '@prisma/client'; |
||||
|
import { Subject } from 'rxjs'; |
||||
|
import { takeUntil } from 'rxjs/operators'; |
||||
|
|
||||
|
@Component({ |
||||
|
changeDetection: ChangeDetectionStrategy.OnPush, |
||||
|
selector: 'gf-admin-market-data', |
||||
|
styleUrls: ['./admin-market-data.scss'], |
||||
|
templateUrl: './admin-market-data.html' |
||||
|
}) |
||||
|
export class AdminMarketDataComponent implements OnDestroy, OnInit { |
||||
|
public currentSymbol: string; |
||||
|
public defaultDateFormat = DEFAULT_DATE_FORMAT; |
||||
|
public marketData: AdminMarketDataItem[] = []; |
||||
|
public marketDataDetails: MarketData[] = []; |
||||
|
|
||||
|
private unsubscribeSubject = new Subject<void>(); |
||||
|
|
||||
|
/** |
||||
|
* @constructor |
||||
|
*/ |
||||
|
public constructor( |
||||
|
private changeDetectorRef: ChangeDetectorRef, |
||||
|
private dataService: DataService |
||||
|
) {} |
||||
|
|
||||
|
/** |
||||
|
* Initializes the controller |
||||
|
*/ |
||||
|
public ngOnInit() { |
||||
|
this.fetchAdminMarketData(); |
||||
|
} |
||||
|
|
||||
|
public setCurrentSymbol(aSymbol: string) { |
||||
|
this.marketDataDetails = []; |
||||
|
|
||||
|
if (this.currentSymbol === aSymbol) { |
||||
|
this.currentSymbol = ''; |
||||
|
} else { |
||||
|
this.currentSymbol = aSymbol; |
||||
|
|
||||
|
this.fetchAdminMarketDataBySymbol(this.currentSymbol); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
public ngOnDestroy() { |
||||
|
this.unsubscribeSubject.next(); |
||||
|
this.unsubscribeSubject.complete(); |
||||
|
} |
||||
|
|
||||
|
private fetchAdminMarketData() { |
||||
|
this.dataService |
||||
|
.fetchAdminMarketData() |
||||
|
.pipe(takeUntil(this.unsubscribeSubject)) |
||||
|
.subscribe(({ marketData }) => { |
||||
|
this.marketData = marketData; |
||||
|
|
||||
|
this.changeDetectorRef.markForCheck(); |
||||
|
}); |
||||
|
} |
||||
|
|
||||
|
private fetchAdminMarketDataBySymbol(aSymbol: string) { |
||||
|
this.dataService |
||||
|
.fetchAdminMarketDataBySymbol(aSymbol) |
||||
|
.pipe(takeUntil(this.unsubscribeSubject)) |
||||
|
.subscribe(({ marketData }) => { |
||||
|
this.marketDataDetails = marketData; |
||||
|
|
||||
|
this.changeDetectorRef.markForCheck(); |
||||
|
}); |
||||
|
} |
||||
|
} |
@ -0,0 +1,37 @@ |
|||||
|
<div class="container"> |
||||
|
<div class="row"> |
||||
|
<div class="col"> |
||||
|
<table class="gf-table w-100"> |
||||
|
<thead> |
||||
|
<tr class="mat-header-row"> |
||||
|
<th class="mat-header-cell px-1 py-2 text-right" i18n>#</th> |
||||
|
<th class="mat-header-cell px-1 py-2" i18n>Symbol</th> |
||||
|
<th class="mat-header-cell px-1 py-2" i18n>First transaction</th> |
||||
|
</tr> |
||||
|
</thead> |
||||
|
<tbody> |
||||
|
<ng-container *ngFor="let item of marketData; let i = index"> |
||||
|
<tr |
||||
|
class="cursor-pointer mat-row" |
||||
|
(click)="setCurrentSymbol(item.symbol)" |
||||
|
> |
||||
|
<td class="mat-cell px-1 py-2 text-right">{{ i + 1 }}</td> |
||||
|
<td class="mat-cell px-1 py-2">{{ item.symbol }}</td> |
||||
|
<td class="mat-cell px-1 py-2"> |
||||
|
{{ (item.date | date: defaultDateFormat) ?? '' }} |
||||
|
</td> |
||||
|
</tr> |
||||
|
<tr *ngIf="currentSymbol === item.symbol" class="mat-row"> |
||||
|
<td></td> |
||||
|
<td colspan="2"> |
||||
|
<gf-admin-market-data-detail |
||||
|
[marketData]="marketDataDetails" |
||||
|
></gf-admin-market-data-detail> |
||||
|
</td> |
||||
|
</tr> |
||||
|
</ng-container> |
||||
|
</tbody> |
||||
|
</table> |
||||
|
</div> |
||||
|
</div> |
||||
|
</div> |
@ -0,0 +1,12 @@ |
|||||
|
import { CommonModule } from '@angular/common'; |
||||
|
import { CUSTOM_ELEMENTS_SCHEMA, NgModule } from '@angular/core'; |
||||
|
import { GfAdminMarketDataDetailModule } from '@ghostfolio/client/components/admin-market-data-detail/admin-market-data-detail.module'; |
||||
|
|
||||
|
import { AdminMarketDataComponent } from './admin-market-data.component'; |
||||
|
|
||||
|
@NgModule({ |
||||
|
declarations: [AdminMarketDataComponent], |
||||
|
imports: [CommonModule, GfAdminMarketDataDetailModule], |
||||
|
schemas: [CUSTOM_ELEMENTS_SCHEMA] |
||||
|
}) |
||||
|
export class GfAdminMarketDataModule {} |
@ -0,0 +1,5 @@ |
|||||
|
@import '~apps/client/src/styles/ghostfolio-style'; |
||||
|
|
||||
|
:host { |
||||
|
display: block; |
||||
|
} |
@ -0,0 +1,5 @@ |
|||||
|
import { MarketData } from '@prisma/client'; |
||||
|
|
||||
|
export interface AdminMarketDataDetails { |
||||
|
marketData: MarketData[]; |
||||
|
} |
@ -0,0 +1,7 @@ |
|||||
|
export interface AdminMarketData { |
||||
|
marketData: AdminMarketDataItem[]; |
||||
|
} |
||||
|
|
||||
|
export interface AdminMarketDataItem { |
||||
|
symbol: string; |
||||
|
} |
Loading…
Reference in new issue