mirror of https://github.com/ghostfolio/ghostfolio
105 changed files with 3305 additions and 1349 deletions
@ -0,0 +1,6 @@ |
|||
const { composePlugins, withNx } = require('@nx/webpack'); |
|||
|
|||
module.exports = composePlugins(withNx(), (config, { options, context }) => { |
|||
// Customize webpack config here
|
|||
return config; |
|||
}); |
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
@ -1,226 +1,273 @@ |
|||
/* Variable fonts usage: |
|||
:root { font-family: "Inter", sans-serif; } |
|||
@supports (font-variation-settings: normal) { |
|||
:root { font-family: "InterVariable", sans-serif; font-optical-sizing: auto; } |
|||
} */ |
|||
@font-face { |
|||
font-family: InterVariable; |
|||
font-style: normal; |
|||
font-weight: 100 900; |
|||
font-display: swap; |
|||
src: url('InterVariable.woff2') format('woff2'); |
|||
} |
|||
@font-face { |
|||
font-family: InterVariable; |
|||
font-style: italic; |
|||
font-weight: 100 900; |
|||
font-display: swap; |
|||
src: url('InterVariable-Italic.woff2') format('woff2'); |
|||
} |
|||
|
|||
/* static fonts */ |
|||
@font-face { |
|||
font-family: 'Inter'; |
|||
font-style: normal; |
|||
font-weight: 100; |
|||
font-display: swap; |
|||
src: |
|||
url('Inter-Thin.woff2?v=3.19') format('woff2'), |
|||
url('Inter-Thin.woff?v=3.19') format('woff'); |
|||
src: url('Inter-Thin.woff2') format('woff2'); |
|||
} |
|||
|
|||
@font-face { |
|||
font-family: 'Inter'; |
|||
font-style: italic; |
|||
font-weight: 100; |
|||
font-display: swap; |
|||
src: |
|||
url('Inter-ThinItalic.woff2?v=3.19') format('woff2'), |
|||
url('Inter-ThinItalic.woff?v=3.19') format('woff'); |
|||
src: url('Inter-ThinItalic.woff2') format('woff2'); |
|||
} |
|||
|
|||
@font-face { |
|||
font-family: 'Inter'; |
|||
font-style: normal; |
|||
font-weight: 200; |
|||
font-display: swap; |
|||
src: |
|||
url('Inter-ExtraLight.woff2?v=3.19') format('woff2'), |
|||
url('Inter-ExtraLight.woff?v=3.19') format('woff'); |
|||
src: url('Inter-ExtraLight.woff2') format('woff2'); |
|||
} |
|||
|
|||
@font-face { |
|||
font-family: 'Inter'; |
|||
font-style: italic; |
|||
font-weight: 200; |
|||
font-display: swap; |
|||
src: |
|||
url('Inter-ExtraLightItalic.woff2?v=3.19') format('woff2'), |
|||
url('Inter-ExtraLightItalic.woff?v=3.19') format('woff'); |
|||
src: url('Inter-ExtraLightItalic.woff2') format('woff2'); |
|||
} |
|||
|
|||
@font-face { |
|||
font-family: 'Inter'; |
|||
font-style: normal; |
|||
font-weight: 300; |
|||
font-display: swap; |
|||
src: |
|||
url('Inter-Light.woff2?v=3.19') format('woff2'), |
|||
url('Inter-Light.woff?v=3.19') format('woff'); |
|||
src: url('Inter-Light.woff2') format('woff2'); |
|||
} |
|||
|
|||
@font-face { |
|||
font-family: 'Inter'; |
|||
font-style: italic; |
|||
font-weight: 300; |
|||
font-display: swap; |
|||
src: |
|||
url('Inter-LightItalic.woff2?v=3.19') format('woff2'), |
|||
url('Inter-LightItalic.woff?v=3.19') format('woff'); |
|||
src: url('Inter-LightItalic.woff2') format('woff2'); |
|||
} |
|||
|
|||
@font-face { |
|||
font-family: 'Inter'; |
|||
font-style: normal; |
|||
font-weight: 400; |
|||
font-display: swap; |
|||
src: |
|||
url('Inter-Regular.woff2?v=3.19') format('woff2'), |
|||
url('Inter-Regular.woff?v=3.19') format('woff'); |
|||
src: url('Inter-Regular.woff2') format('woff2'); |
|||
} |
|||
|
|||
@font-face { |
|||
font-family: 'Inter'; |
|||
font-style: italic; |
|||
font-weight: 400; |
|||
font-display: swap; |
|||
src: |
|||
url('Inter-Italic.woff2?v=3.19') format('woff2'), |
|||
url('Inter-Italic.woff?v=3.19') format('woff'); |
|||
src: url('Inter-Italic.woff2') format('woff2'); |
|||
} |
|||
|
|||
@font-face { |
|||
font-family: 'Inter'; |
|||
font-style: normal; |
|||
font-weight: 500; |
|||
font-display: swap; |
|||
src: |
|||
url('Inter-Medium.woff2?v=3.19') format('woff2'), |
|||
url('Inter-Medium.woff?v=3.19') format('woff'); |
|||
src: url('Inter-Medium.woff2') format('woff2'); |
|||
} |
|||
|
|||
@font-face { |
|||
font-family: 'Inter'; |
|||
font-style: italic; |
|||
font-weight: 500; |
|||
font-display: swap; |
|||
src: |
|||
url('Inter-MediumItalic.woff2?v=3.19') format('woff2'), |
|||
url('Inter-MediumItalic.woff?v=3.19') format('woff'); |
|||
src: url('Inter-MediumItalic.woff2') format('woff2'); |
|||
} |
|||
|
|||
@font-face { |
|||
font-family: 'Inter'; |
|||
font-style: normal; |
|||
font-weight: 600; |
|||
font-display: swap; |
|||
src: |
|||
url('Inter-SemiBold.woff2?v=3.19') format('woff2'), |
|||
url('Inter-SemiBold.woff?v=3.19') format('woff'); |
|||
src: url('Inter-SemiBold.woff2') format('woff2'); |
|||
} |
|||
|
|||
@font-face { |
|||
font-family: 'Inter'; |
|||
font-style: italic; |
|||
font-weight: 600; |
|||
font-display: swap; |
|||
src: |
|||
url('Inter-SemiBoldItalic.woff2?v=3.19') format('woff2'), |
|||
url('Inter-SemiBoldItalic.woff?v=3.19') format('woff'); |
|||
src: url('Inter-SemiBoldItalic.woff2') format('woff2'); |
|||
} |
|||
|
|||
@font-face { |
|||
font-family: 'Inter'; |
|||
font-style: normal; |
|||
font-weight: 700; |
|||
font-display: swap; |
|||
src: |
|||
url('Inter-Bold.woff2?v=3.19') format('woff2'), |
|||
url('Inter-Bold.woff?v=3.19') format('woff'); |
|||
src: url('Inter-Bold.woff2') format('woff2'); |
|||
} |
|||
|
|||
@font-face { |
|||
font-family: 'Inter'; |
|||
font-style: italic; |
|||
font-weight: 700; |
|||
font-display: swap; |
|||
src: |
|||
url('Inter-BoldItalic.woff2?v=3.19') format('woff2'), |
|||
url('Inter-BoldItalic.woff?v=3.19') format('woff'); |
|||
src: url('Inter-BoldItalic.woff2') format('woff2'); |
|||
} |
|||
|
|||
@font-face { |
|||
font-family: 'Inter'; |
|||
font-style: normal; |
|||
font-weight: 800; |
|||
font-display: swap; |
|||
src: |
|||
url('Inter-ExtraBold.woff2?v=3.19') format('woff2'), |
|||
url('Inter-ExtraBold.woff?v=3.19') format('woff'); |
|||
src: url('Inter-ExtraBold.woff2') format('woff2'); |
|||
} |
|||
|
|||
@font-face { |
|||
font-family: 'Inter'; |
|||
font-style: italic; |
|||
font-weight: 800; |
|||
font-display: swap; |
|||
src: |
|||
url('Inter-ExtraBoldItalic.woff2?v=3.19') format('woff2'), |
|||
url('Inter-ExtraBoldItalic.woff?v=3.19') format('woff'); |
|||
src: url('Inter-ExtraBoldItalic.woff2') format('woff2'); |
|||
} |
|||
|
|||
@font-face { |
|||
font-family: 'Inter'; |
|||
font-style: normal; |
|||
font-weight: 900; |
|||
font-display: swap; |
|||
src: |
|||
url('Inter-Black.woff2?v=3.19') format('woff2'), |
|||
url('Inter-Black.woff?v=3.19') format('woff'); |
|||
src: url('Inter-Black.woff2') format('woff2'); |
|||
} |
|||
|
|||
@font-face { |
|||
font-family: 'Inter'; |
|||
font-style: italic; |
|||
font-weight: 900; |
|||
font-display: swap; |
|||
src: |
|||
url('Inter-BlackItalic.woff2?v=3.19') format('woff2'), |
|||
url('Inter-BlackItalic.woff?v=3.19') format('woff'); |
|||
src: url('Inter-BlackItalic.woff2') format('woff2'); |
|||
} |
|||
|
|||
/* ------------------------------------------------------- |
|||
Variable font. |
|||
Usage: |
|||
|
|||
html { font-family: 'Inter', sans-serif; } |
|||
@supports (font-variation-settings: normal) { |
|||
html { font-family: 'Inter var', sans-serif; } |
|||
} |
|||
*/ |
|||
@font-face { |
|||
font-family: 'Inter var'; |
|||
font-weight: 100 900; |
|||
font-family: 'InterDisplay'; |
|||
font-style: normal; |
|||
font-weight: 100; |
|||
font-display: swap; |
|||
src: url('InterDisplay-Thin.woff2') format('woff2'); |
|||
} |
|||
@font-face { |
|||
font-family: 'InterDisplay'; |
|||
font-style: italic; |
|||
font-weight: 100; |
|||
font-display: swap; |
|||
src: url('InterDisplay-ThinItalic.woff2') format('woff2'); |
|||
} |
|||
@font-face { |
|||
font-family: 'InterDisplay'; |
|||
font-style: normal; |
|||
font-named-instance: 'Regular'; |
|||
src: url('Inter-roman.var.woff2?v=3.19') format('woff2'); |
|||
font-weight: 200; |
|||
font-display: swap; |
|||
src: url('InterDisplay-ExtraLight.woff2') format('woff2'); |
|||
} |
|||
@font-face { |
|||
font-family: 'Inter var'; |
|||
font-weight: 100 900; |
|||
font-family: 'InterDisplay'; |
|||
font-style: italic; |
|||
font-weight: 200; |
|||
font-display: swap; |
|||
src: url('InterDisplay-ExtraLightItalic.woff2') format('woff2'); |
|||
} |
|||
@font-face { |
|||
font-family: 'InterDisplay'; |
|||
font-style: normal; |
|||
font-weight: 300; |
|||
font-display: swap; |
|||
src: url('InterDisplay-Light.woff2') format('woff2'); |
|||
} |
|||
@font-face { |
|||
font-family: 'InterDisplay'; |
|||
font-style: italic; |
|||
font-named-instance: 'Italic'; |
|||
src: url('Inter-italic.var.woff2?v=3.19') format('woff2'); |
|||
font-weight: 300; |
|||
font-display: swap; |
|||
src: url('InterDisplay-LightItalic.woff2') format('woff2'); |
|||
} |
|||
|
|||
/* -------------------------------------------------------------------------- |
|||
[EXPERIMENTAL] Multi-axis, single variable font. |
|||
|
|||
Slant axis is not yet widely supported (as of February 2019) and thus this |
|||
multi-axis single variable font is opt-in rather than the default. |
|||
|
|||
When using this, you will probably need to set font-variation-settings |
|||
explicitly, e.g. |
|||
|
|||
* { font-variation-settings: "slnt" 0deg } |
|||
.italic { font-variation-settings: "slnt" 10deg } |
|||
|
|||
*/ |
|||
@font-face { |
|||
font-family: 'Inter var experimental'; |
|||
font-weight: 100 900; |
|||
font-family: 'InterDisplay'; |
|||
font-style: normal; |
|||
font-weight: 400; |
|||
font-display: swap; |
|||
src: url('InterDisplay-Regular.woff2') format('woff2'); |
|||
} |
|||
@font-face { |
|||
font-family: 'InterDisplay'; |
|||
font-style: italic; |
|||
font-weight: 400; |
|||
font-display: swap; |
|||
src: url('InterDisplay-Italic.woff2') format('woff2'); |
|||
} |
|||
@font-face { |
|||
font-family: 'InterDisplay'; |
|||
font-style: normal; |
|||
font-weight: 500; |
|||
font-display: swap; |
|||
src: url('InterDisplay-Medium.woff2') format('woff2'); |
|||
} |
|||
@font-face { |
|||
font-family: 'InterDisplay'; |
|||
font-style: italic; |
|||
font-weight: 500; |
|||
font-display: swap; |
|||
src: url('InterDisplay-MediumItalic.woff2') format('woff2'); |
|||
} |
|||
@font-face { |
|||
font-family: 'InterDisplay'; |
|||
font-style: normal; |
|||
font-weight: 600; |
|||
font-display: swap; |
|||
src: url('InterDisplay-SemiBold.woff2') format('woff2'); |
|||
} |
|||
@font-face { |
|||
font-family: 'InterDisplay'; |
|||
font-style: italic; |
|||
font-weight: 600; |
|||
font-display: swap; |
|||
src: url('InterDisplay-SemiBoldItalic.woff2') format('woff2'); |
|||
} |
|||
@font-face { |
|||
font-family: 'InterDisplay'; |
|||
font-style: normal; |
|||
font-weight: 700; |
|||
font-display: swap; |
|||
src: url('InterDisplay-Bold.woff2') format('woff2'); |
|||
} |
|||
@font-face { |
|||
font-family: 'InterDisplay'; |
|||
font-style: italic; |
|||
font-weight: 700; |
|||
font-display: swap; |
|||
src: url('InterDisplay-BoldItalic.woff2') format('woff2'); |
|||
} |
|||
@font-face { |
|||
font-family: 'InterDisplay'; |
|||
font-style: normal; |
|||
font-weight: 800; |
|||
font-display: swap; |
|||
src: url('InterDisplay-ExtraBold.woff2') format('woff2'); |
|||
} |
|||
@font-face { |
|||
font-family: 'InterDisplay'; |
|||
font-style: italic; |
|||
font-weight: 800; |
|||
font-display: swap; |
|||
src: url('InterDisplay-ExtraBoldItalic.woff2') format('woff2'); |
|||
} |
|||
@font-face { |
|||
font-family: 'InterDisplay'; |
|||
font-style: normal; |
|||
font-weight: 900; |
|||
font-display: swap; |
|||
src: url('InterDisplay-Black.woff2') format('woff2'); |
|||
} |
|||
@font-face { |
|||
font-family: 'InterDisplay'; |
|||
font-style: italic; |
|||
font-weight: 900; |
|||
font-display: swap; |
|||
font-style: oblique 0deg 10deg; |
|||
src: url('Inter.var.woff2?v=3.19') format('woff2'); |
|||
src: url('InterDisplay-BlackItalic.woff2') format('woff2'); |
|||
} |
|||
|
File diff suppressed because it is too large
@ -0,0 +1,492 @@ |
|||
<div *ngIf="hasPermissionToCreateActivity" class="d-flex justify-content-end"> |
|||
<button |
|||
class="align-items-center d-flex" |
|||
mat-stroked-button |
|||
(click)="onImport()" |
|||
> |
|||
<ion-icon class="mr-2" name="cloud-upload-outline"></ion-icon> |
|||
<ng-container i18n>Import Activities</ng-container>... |
|||
</button> |
|||
<button |
|||
*ngIf="hasPermissionToExportActivities" |
|||
class="mx-1 no-min-width px-2" |
|||
mat-stroked-button |
|||
[matMenuTriggerFor]="activitiesMenu" |
|||
(click)="$event.stopPropagation()" |
|||
> |
|||
<ion-icon name="ellipsis-vertical"></ion-icon> |
|||
</button> |
|||
<mat-menu #activitiesMenu="matMenu" xPosition="before"> |
|||
<button |
|||
mat-menu-item |
|||
[disabled]="dataSource?.data.length === 0" |
|||
(click)="onImportDividends()" |
|||
> |
|||
<span class="align-items-center d-flex"> |
|||
<ion-icon class="mr-2" name="color-wand-outline"></ion-icon> |
|||
<ng-container i18n>Import Dividends</ng-container>... |
|||
</span> |
|||
</button> |
|||
<button |
|||
*ngIf="hasPermissionToExportActivities" |
|||
class="align-items-center d-flex" |
|||
mat-menu-item |
|||
[disabled]="dataSource?.data.length === 0" |
|||
(click)="onExport()" |
|||
> |
|||
<span class="align-items-center d-flex"> |
|||
<ion-icon class="mr-2" name="cloud-download-outline"></ion-icon> |
|||
<span i18n>Export Activities</span> |
|||
</span> |
|||
</button> |
|||
<button |
|||
*ngIf="hasPermissionToExportActivities" |
|||
class="align-items-center d-flex" |
|||
mat-menu-item |
|||
[disabled]="!hasDrafts" |
|||
(click)="onExportDrafts()" |
|||
> |
|||
<span class="align-items-center d-flex"> |
|||
<ion-icon class="mr-2" name="calendar-clear-outline"></ion-icon> |
|||
<span i18n>Export Drafts as ICS</span> |
|||
</span> |
|||
</button> |
|||
<button |
|||
class="align-items-center d-flex" |
|||
mat-menu-item |
|||
(click)="onDeleteAllActivities()" |
|||
> |
|||
<span class="align-items-center d-flex"> |
|||
<ion-icon class="mr-2" name="trash-outline"></ion-icon> |
|||
<span i18n>Delete all Activities</span> |
|||
</span> |
|||
</button> |
|||
</mat-menu> |
|||
</div> |
|||
|
|||
<div class="activities"> |
|||
<table |
|||
class="gf-table w-100" |
|||
mat-table |
|||
matSort |
|||
[dataSource]="dataSource" |
|||
[matSortActive]="sortColumn" |
|||
[matSortDirection]="sortDirection" |
|||
[matSortDisabled]="sortDisabled" |
|||
> |
|||
<ng-container matColumnDef="select" sticky> |
|||
<th *matHeaderCellDef class="px-1" mat-header-cell> |
|||
<mat-checkbox |
|||
color="primary" |
|||
[checked]=" |
|||
areAllRowsSelected() && !hasErrors && selectedRows.hasValue() |
|||
" |
|||
[disabled]="hasErrors" |
|||
[indeterminate]="selectedRows.hasValue() && !areAllRowsSelected()" |
|||
(change)="$event ? toggleAllRows() : null" |
|||
></mat-checkbox> |
|||
</th> |
|||
<td *matCellDef="let element" class="px-1" mat-cell> |
|||
<mat-checkbox |
|||
color="primary" |
|||
[checked]="element.error ? false : selectedRows.isSelected(element)" |
|||
[disabled]="element.error" |
|||
(change)="$event ? selectedRows.toggle(element) : null" |
|||
(click)="$event.stopPropagation()" |
|||
></mat-checkbox> |
|||
</td> |
|||
</ng-container> |
|||
|
|||
<ng-container matColumnDef="importStatus"> |
|||
<th *matHeaderCellDef class="px-1" mat-header-cell> |
|||
<ng-container i18n></ng-container> |
|||
</th> |
|||
<td *matCellDef="let element" class="px-1" mat-cell> |
|||
<div |
|||
*ngIf="element.error" |
|||
class="d-flex" |
|||
matTooltipPosition="above" |
|||
[matTooltip]="element.error.message" |
|||
> |
|||
<ion-icon class="text-danger" name="alert-circle-outline"></ion-icon> |
|||
</div> |
|||
</td> |
|||
</ng-container> |
|||
|
|||
<ng-container matColumnDef="icon"> |
|||
<th *matHeaderCellDef class="px-1" mat-header-cell></th> |
|||
<td *matCellDef="let element" class="px-1 text-center" mat-cell> |
|||
<gf-symbol-icon |
|||
[dataSource]="element.SymbolProfile?.dataSource" |
|||
[symbol]="element.SymbolProfile?.symbol" |
|||
[tooltip]="element.SymbolProfile?.name" |
|||
></gf-symbol-icon> |
|||
<div>{{ element.dataSource }}</div> |
|||
</td> |
|||
</ng-container> |
|||
|
|||
<ng-container matColumnDef="nameWithSymbol"> |
|||
<th *matHeaderCellDef class="px-1" mat-header-cell> |
|||
<ng-container i18n>Name</ng-container> |
|||
</th> |
|||
<td *matCellDef="let element" class="line-height-1 px-1" mat-cell> |
|||
<div class="d-flex align-items-center"> |
|||
<div> |
|||
<span class="text-truncate">{{ element.SymbolProfile?.name }}</span> |
|||
<span |
|||
*ngIf="element.isDraft" |
|||
class="badge badge-secondary ml-1" |
|||
i18n |
|||
>Draft</span |
|||
> |
|||
</div> |
|||
</div> |
|||
<div *ngIf="!isUUID(element.SymbolProfile?.symbol)"> |
|||
<small class="text-muted">{{ |
|||
element.SymbolProfile?.symbol | gfSymbol |
|||
}}</small> |
|||
</div> |
|||
</td> |
|||
</ng-container> |
|||
|
|||
<ng-container matColumnDef="type"> |
|||
<th *matHeaderCellDef class="px-1" mat-header-cell mat-sort-header> |
|||
<ng-container i18n>Type</ng-container> |
|||
</th> |
|||
<td *matCellDef="let element" class="px-1" mat-cell> |
|||
<gf-activity-type [activityType]="element.type"></gf-activity-type> |
|||
</td> |
|||
</ng-container> |
|||
|
|||
<ng-container matColumnDef="date"> |
|||
<th *matHeaderCellDef class="px-1" mat-header-cell mat-sort-header> |
|||
<ng-container i18n>Date</ng-container> |
|||
</th> |
|||
<td *matCellDef="let element" class="px-1" mat-cell> |
|||
<div class="d-flex"> |
|||
{{ element.date | date: defaultDateFormat }} |
|||
</div> |
|||
</td> |
|||
</ng-container> |
|||
|
|||
<ng-container matColumnDef="quantity"> |
|||
<th |
|||
*matHeaderCellDef |
|||
class="d-none d-lg-table-cell justify-content-end px-1" |
|||
mat-header-cell |
|||
mat-sort-header |
|||
> |
|||
<ng-container i18n>Quantity</ng-container> |
|||
</th> |
|||
<td |
|||
*matCellDef="let element" |
|||
class="d-none d-lg-table-cell px-1" |
|||
mat-cell |
|||
> |
|||
<div class="d-flex justify-content-end"> |
|||
<gf-value |
|||
[isCurrency]="true" |
|||
[locale]="locale" |
|||
[value]="isLoading ? undefined : element.quantity" |
|||
></gf-value> |
|||
</div> |
|||
</td> |
|||
</ng-container> |
|||
|
|||
<ng-container matColumnDef="unitPrice"> |
|||
<th |
|||
*matHeaderCellDef |
|||
class="d-none d-lg-table-cell justify-content-end px-1" |
|||
mat-header-cell |
|||
mat-sort-header |
|||
> |
|||
<ng-container i18n>Unit Price</ng-container> |
|||
</th> |
|||
<td |
|||
*matCellDef="let element" |
|||
class="d-none d-lg-table-cell px-1" |
|||
mat-cell |
|||
> |
|||
<div class="d-flex justify-content-end"> |
|||
<gf-value |
|||
[isCurrency]="true" |
|||
[locale]="locale" |
|||
[value]="isLoading ? undefined : element.unitPrice" |
|||
></gf-value> |
|||
</div> |
|||
</td> |
|||
</ng-container> |
|||
|
|||
<ng-container matColumnDef="fee"> |
|||
<th |
|||
*matHeaderCellDef |
|||
class="d-none d-lg-table-cell justify-content-end px-1" |
|||
mat-header-cell |
|||
mat-sort-header |
|||
> |
|||
<ng-container i18n>Fee</ng-container> |
|||
</th> |
|||
<td |
|||
*matCellDef="let element" |
|||
class="d-none d-lg-table-cell px-1" |
|||
mat-cell |
|||
> |
|||
<div class="d-flex justify-content-end"> |
|||
<gf-value |
|||
[isCurrency]="true" |
|||
[locale]="locale" |
|||
[value]="isLoading ? undefined : element.fee" |
|||
></gf-value> |
|||
</div> |
|||
</td> |
|||
</ng-container> |
|||
|
|||
<ng-container matColumnDef="value"> |
|||
<th |
|||
*matHeaderCellDef |
|||
class="d-none d-lg-table-cell justify-content-end px-1" |
|||
mat-header-cell |
|||
> |
|||
<ng-container i18n>Value</ng-container> |
|||
</th> |
|||
<td |
|||
*matCellDef="let element" |
|||
class="d-none d-lg-table-cell px-1" |
|||
mat-cell |
|||
> |
|||
<div class="d-flex justify-content-end"> |
|||
<gf-value |
|||
[isCurrency]="true" |
|||
[locale]="locale" |
|||
[value]="isLoading ? undefined : element.value" |
|||
></gf-value> |
|||
</div> |
|||
</td> |
|||
</ng-container> |
|||
|
|||
<ng-container matColumnDef="currency"> |
|||
<th *matHeaderCellDef class="d-none d-lg-table-cell px-1" mat-header-cell> |
|||
<ng-container i18n>Currency</ng-container> |
|||
</th> |
|||
<td |
|||
*matCellDef="let element" |
|||
class="d-none d-lg-table-cell px-1" |
|||
mat-cell |
|||
> |
|||
{{ element.SymbolProfile?.currency }} |
|||
</td> |
|||
</ng-container> |
|||
|
|||
<ng-container matColumnDef="valueInBaseCurrency"> |
|||
<th |
|||
*matHeaderCellDef |
|||
class="d-lg-none d-xl-none justify-content-end px-1" |
|||
mat-header-cell |
|||
> |
|||
<ng-container i18n>Value</ng-container> |
|||
</th> |
|||
<td *matCellDef="let element" class="d-lg-none d-xl-none px-1" mat-cell> |
|||
<div class="d-flex justify-content-end"> |
|||
<gf-value |
|||
[isCurrency]="true" |
|||
[locale]="locale" |
|||
[value]="isLoading ? undefined : element.valueInBaseCurrency" |
|||
></gf-value> |
|||
</div> |
|||
</td> |
|||
</ng-container> |
|||
|
|||
<ng-container matColumnDef="account"> |
|||
<th *matHeaderCellDef class="px-1" mat-header-cell> |
|||
<span class="d-none d-lg-block" i18n>Account</span> |
|||
</th> |
|||
<td *matCellDef="let element" class="px-1" mat-cell> |
|||
<div class="d-flex"> |
|||
<gf-symbol-icon |
|||
*ngIf="element.Account?.Platform?.url" |
|||
class="mr-1" |
|||
[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="comment"> |
|||
<th |
|||
*matHeaderCellDef |
|||
class="d-none d-lg-table-cell px-1" |
|||
mat-header-cell |
|||
></th> |
|||
<td |
|||
*matCellDef="let element" |
|||
class="d-none d-lg-table-cell px-1" |
|||
mat-cell |
|||
> |
|||
<button |
|||
*ngIf="element.comment" |
|||
class="mx-1 no-min-width px-2" |
|||
mat-button |
|||
title="Note" |
|||
(click)="onOpenComment(element.comment); $event.stopPropagation()" |
|||
> |
|||
<ion-icon name="document-text-outline"></ion-icon> |
|||
</button> |
|||
</td> |
|||
</ng-container> |
|||
|
|||
<ng-container matColumnDef="actions" stickyEnd> |
|||
<th *matHeaderCellDef class="px-1 text-center" mat-header-cell> |
|||
<button |
|||
*ngIf=" |
|||
!hasPermissionToCreateActivity && hasPermissionToExportActivities |
|||
" |
|||
class="mx-1 no-min-width px-2" |
|||
mat-button |
|||
[matMenuTriggerFor]="activitiesMenu" |
|||
(click)="$event.stopPropagation()" |
|||
> |
|||
<ion-icon name="ellipsis-vertical"></ion-icon> |
|||
</button> |
|||
<mat-menu #activitiesMenu="matMenu" xPosition="before"> |
|||
<button |
|||
*ngIf="hasPermissionToCreateActivity" |
|||
class="align-items-center d-flex" |
|||
mat-menu-item |
|||
(click)="onImport()" |
|||
> |
|||
<span class="align-items-center d-flex"> |
|||
<ion-icon class="mr-2" name="cloud-upload-outline"></ion-icon> |
|||
<ng-container i18n>Import Activities</ng-container>... |
|||
</span> |
|||
</button> |
|||
<button |
|||
*ngIf="hasPermissionToCreateActivity" |
|||
mat-menu-item |
|||
[disabled]="dataSource?.data.length === 0" |
|||
(click)="onImportDividends()" |
|||
> |
|||
<span class="align-items-center d-flex"> |
|||
<ion-icon class="mr-2" name="color-wand-outline"></ion-icon> |
|||
<ng-container i18n>Import Dividends</ng-container>... |
|||
</span> |
|||
</button> |
|||
<button |
|||
*ngIf="hasPermissionToExportActivities" |
|||
class="align-items-center d-flex" |
|||
mat-menu-item |
|||
[disabled]="dataSource?.data.length === 0" |
|||
(click)="onExport()" |
|||
> |
|||
<span class="align-items-center d-flex"> |
|||
<ion-icon class="mr-2" name="cloud-download-outline"></ion-icon> |
|||
<span i18n>Export Activities</span> |
|||
</span> |
|||
</button> |
|||
<button |
|||
*ngIf="hasPermissionToExportActivities" |
|||
class="align-items-center d-flex" |
|||
mat-menu-item |
|||
[disabled]="!hasDrafts" |
|||
(click)="onExportDrafts()" |
|||
> |
|||
<span class="align-items-center d-flex"> |
|||
<ion-icon class="mr-2" name="calendar-clear-outline"></ion-icon> |
|||
<span i18n>Export Drafts as ICS</span> |
|||
</span> |
|||
</button> |
|||
</mat-menu> |
|||
</th> |
|||
<td *matCellDef="let element" class="px-1 text-center" mat-cell> |
|||
<button |
|||
*ngIf="showActions" |
|||
class="mx-1 no-min-width px-2" |
|||
mat-button |
|||
[matMenuTriggerFor]="activityMenu" |
|||
(click)="$event.stopPropagation()" |
|||
> |
|||
<ion-icon name="ellipsis-horizontal"></ion-icon> |
|||
</button> |
|||
<mat-menu #activityMenu="matMenu" xPosition="before"> |
|||
<button mat-menu-item (click)="onUpdateActivity(element)"> |
|||
<span class="align-items-center d-flex"> |
|||
<ion-icon class="mr-2" name="create-outline"></ion-icon> |
|||
<span i18n>Edit</span> |
|||
</span> |
|||
</button> |
|||
<button mat-menu-item (click)="onCloneActivity(element)"> |
|||
<span class="align-items-center d-flex"> |
|||
<ion-icon class="mr-2" name="copy-outline"></ion-icon> |
|||
<span i18n>Clone</span> |
|||
</span> |
|||
</button> |
|||
<button |
|||
mat-menu-item |
|||
[disabled]="!element.isDraft" |
|||
(click)="onExportDraft(element.id)" |
|||
> |
|||
<span class="align-items-center d-flex"> |
|||
<ion-icon class="mr-2" name="calendar-clear-outline"></ion-icon> |
|||
<span i18n>Export Draft as ICS</span> |
|||
</span> |
|||
</button> |
|||
<button mat-menu-item (click)="onDeleteActivity(element.id)"> |
|||
<span class="align-items-center d-flex"> |
|||
<ion-icon class="mr-2" name="trash-outline"></ion-icon> |
|||
<span i18n>Delete</span> |
|||
</span> |
|||
</button> |
|||
</mat-menu> |
|||
</td> |
|||
</ng-container> |
|||
|
|||
<tr *matHeaderRowDef="displayedColumns" mat-header-row></tr> |
|||
<tr |
|||
*matRowDef="let row; columns: displayedColumns" |
|||
mat-row |
|||
[ngClass]="{ |
|||
'cursor-pointer': |
|||
hasPermissionToOpenDetails && |
|||
!row.isDraft && |
|||
row.type !== 'FEE' && |
|||
row.type !== 'INTEREST' && |
|||
row.type !== 'ITEM' && |
|||
row.type !== 'LIABILITY' |
|||
}" |
|||
(click)="onClickActivity(row)" |
|||
></tr> |
|||
</table> |
|||
</div> |
|||
|
|||
<ngx-skeleton-loader |
|||
*ngIf="isLoading" |
|||
animation="pulse" |
|||
class="px-4 py-3" |
|||
[theme]="{ |
|||
height: '1.5rem', |
|||
width: '100%' |
|||
}" |
|||
></ngx-skeleton-loader> |
|||
|
|||
<mat-paginator |
|||
[length]="totalItems" |
|||
[ngClass]="{ |
|||
'd-none': (isLoading && !totalItems) || totalItems <= pageSize |
|||
}" |
|||
[pageIndex]="pageIndex" |
|||
[pageSize]="pageSize" |
|||
[showFirstLastButtons]="true" |
|||
(page)="onChangePage($event)" |
|||
></mat-paginator> |
|||
|
|||
<div |
|||
*ngIf=" |
|||
dataSource?.data.length === 0 && hasPermissionToCreateActivity && !isLoading |
|||
" |
|||
class="p-3 text-center" |
|||
> |
|||
<gf-no-transactions-info-indicator |
|||
[hasBorder]="false" |
|||
></gf-no-transactions-info-indicator> |
|||
</div> |
@ -0,0 +1,9 @@ |
|||
@import 'apps/client/src/styles/ghostfolio-style'; |
|||
|
|||
:host { |
|||
display: block; |
|||
|
|||
.activities { |
|||
overflow-x: auto; |
|||
} |
|||
} |
@ -0,0 +1,250 @@ |
|||
import { SelectionModel } from '@angular/cdk/collections'; |
|||
import { |
|||
AfterViewInit, |
|||
ChangeDetectionStrategy, |
|||
Component, |
|||
EventEmitter, |
|||
Input, |
|||
OnChanges, |
|||
OnDestroy, |
|||
OnInit, |
|||
Output, |
|||
ViewChild |
|||
} from '@angular/core'; |
|||
import { MatPaginator, PageEvent } from '@angular/material/paginator'; |
|||
import { MatSort, Sort, SortDirection } from '@angular/material/sort'; |
|||
import { MatTableDataSource } from '@angular/material/table'; |
|||
import { Router } from '@angular/router'; |
|||
import { Activity } from '@ghostfolio/api/app/order/interfaces/activities.interface'; |
|||
import { DEFAULT_PAGE_SIZE } from '@ghostfolio/common/config'; |
|||
import { getDateFormatString } from '@ghostfolio/common/helper'; |
|||
import { UniqueAsset } from '@ghostfolio/common/interfaces'; |
|||
import { OrderWithAccount } from '@ghostfolio/common/types'; |
|||
import { isUUID } from 'class-validator'; |
|||
import { endOfToday, isAfter } from 'date-fns'; |
|||
import { Subject, Subscription, takeUntil } from 'rxjs'; |
|||
|
|||
@Component({ |
|||
changeDetection: ChangeDetectionStrategy.OnPush, |
|||
selector: 'gf-activities-table-lazy', |
|||
styleUrls: ['./activities-table-lazy.component.scss'], |
|||
templateUrl: './activities-table-lazy.component.html' |
|||
}) |
|||
export class ActivitiesTableLazyComponent |
|||
implements AfterViewInit, OnChanges, OnDestroy, OnInit |
|||
{ |
|||
@Input() baseCurrency: string; |
|||
@Input() dataSource: MatTableDataSource<Activity>; |
|||
@Input() deviceType: string; |
|||
@Input() hasPermissionToCreateActivity: boolean; |
|||
@Input() hasPermissionToExportActivities: boolean; |
|||
@Input() hasPermissionToOpenDetails = true; |
|||
@Input() locale: string; |
|||
@Input() pageIndex: number; |
|||
@Input() pageSize = DEFAULT_PAGE_SIZE; |
|||
@Input() showActions = true; |
|||
@Input() showCheckbox = false; |
|||
@Input() showFooter = true; |
|||
@Input() showNameColumn = true; |
|||
@Input() sortColumn: string; |
|||
@Input() sortDirection: SortDirection; |
|||
@Input() sortDisabled = false; |
|||
@Input() totalItems = Number.MAX_SAFE_INTEGER; |
|||
|
|||
@Output() activityDeleted = new EventEmitter<string>(); |
|||
@Output() activityToClone = new EventEmitter<OrderWithAccount>(); |
|||
@Output() activityToUpdate = new EventEmitter<OrderWithAccount>(); |
|||
@Output() deleteAllActivities = new EventEmitter<void>(); |
|||
@Output() export = new EventEmitter<string[]>(); |
|||
@Output() exportDrafts = new EventEmitter<string[]>(); |
|||
@Output() import = new EventEmitter<void>(); |
|||
@Output() importDividends = new EventEmitter<UniqueAsset>(); |
|||
@Output() pageChanged = new EventEmitter<PageEvent>(); |
|||
@Output() selectedActivities = new EventEmitter<Activity[]>(); |
|||
@Output() sortChanged = new EventEmitter<Sort>(); |
|||
|
|||
@ViewChild(MatPaginator) paginator: MatPaginator; |
|||
@ViewChild(MatSort) sort: MatSort; |
|||
|
|||
public defaultDateFormat: string; |
|||
public displayedColumns = []; |
|||
public endOfToday = endOfToday(); |
|||
public hasDrafts = false; |
|||
public hasErrors = false; |
|||
public isAfter = isAfter; |
|||
public isLoading = true; |
|||
public isUUID = isUUID; |
|||
public routeQueryParams: Subscription; |
|||
public searchKeywords: string[] = []; |
|||
public selectedRows = new SelectionModel<Activity>(true, []); |
|||
|
|||
private unsubscribeSubject = new Subject<void>(); |
|||
|
|||
public constructor(private router: Router) {} |
|||
|
|||
public ngOnInit() { |
|||
if (this.showCheckbox) { |
|||
this.toggleAllRows(); |
|||
this.selectedRows.changed |
|||
.pipe(takeUntil(this.unsubscribeSubject)) |
|||
.subscribe((selectedRows) => { |
|||
this.selectedActivities.emit(selectedRows.source.selected); |
|||
}); |
|||
} |
|||
} |
|||
|
|||
public ngAfterViewInit() { |
|||
this.sort.sortChange.subscribe((value: Sort) => { |
|||
this.sortChanged.emit(value); |
|||
}); |
|||
} |
|||
|
|||
public areAllRowsSelected() { |
|||
const numSelectedRows = this.selectedRows.selected.length; |
|||
const numTotalRows = this.dataSource.data.length; |
|||
return numSelectedRows === numTotalRows; |
|||
} |
|||
|
|||
public ngOnChanges() { |
|||
this.defaultDateFormat = getDateFormatString(this.locale); |
|||
|
|||
this.displayedColumns = [ |
|||
'select', |
|||
'importStatus', |
|||
'icon', |
|||
'nameWithSymbol', |
|||
'type', |
|||
'date', |
|||
'quantity', |
|||
'unitPrice', |
|||
'fee', |
|||
'value', |
|||
'currency', |
|||
'valueInBaseCurrency', |
|||
'account', |
|||
'comment', |
|||
'actions' |
|||
]; |
|||
|
|||
if (!this.showCheckbox) { |
|||
this.displayedColumns = this.displayedColumns.filter((column) => { |
|||
return column !== 'importStatus' && column !== 'select'; |
|||
}); |
|||
} |
|||
|
|||
if (!this.showNameColumn) { |
|||
this.displayedColumns = this.displayedColumns.filter((column) => { |
|||
return column !== 'nameWithSymbol'; |
|||
}); |
|||
} |
|||
|
|||
if (this.dataSource) { |
|||
this.isLoading = false; |
|||
} |
|||
} |
|||
|
|||
public onChangePage(page: PageEvent) { |
|||
this.pageChanged.emit(page); |
|||
} |
|||
|
|||
public onClickActivity(activity: Activity) { |
|||
if (this.showCheckbox) { |
|||
if (!activity.error) { |
|||
this.selectedRows.toggle(activity); |
|||
} |
|||
} else if ( |
|||
this.hasPermissionToOpenDetails && |
|||
!activity.isDraft && |
|||
activity.type !== 'FEE' && |
|||
activity.type !== 'INTEREST' && |
|||
activity.type !== 'ITEM' && |
|||
activity.type !== 'LIABILITY' |
|||
) { |
|||
this.onOpenPositionDialog({ |
|||
dataSource: activity.SymbolProfile.dataSource, |
|||
symbol: activity.SymbolProfile.symbol |
|||
}); |
|||
} |
|||
} |
|||
|
|||
public onCloneActivity(aActivity: OrderWithAccount) { |
|||
this.activityToClone.emit(aActivity); |
|||
} |
|||
|
|||
public onDeleteActivity(aId: string) { |
|||
const confirmation = confirm( |
|||
$localize`Do you really want to delete this activity?` |
|||
); |
|||
|
|||
if (confirmation) { |
|||
this.activityDeleted.emit(aId); |
|||
} |
|||
} |
|||
|
|||
public onExport() { |
|||
if (this.searchKeywords.length > 0) { |
|||
this.export.emit( |
|||
this.dataSource.filteredData.map((activity) => { |
|||
return activity.id; |
|||
}) |
|||
); |
|||
} else { |
|||
this.export.emit(); |
|||
} |
|||
} |
|||
|
|||
public onExportDraft(aActivityId: string) { |
|||
this.exportDrafts.emit([aActivityId]); |
|||
} |
|||
|
|||
public onExportDrafts() { |
|||
this.exportDrafts.emit( |
|||
this.dataSource.filteredData |
|||
.filter((activity) => { |
|||
return activity.isDraft; |
|||
}) |
|||
.map((activity) => { |
|||
return activity.id; |
|||
}) |
|||
); |
|||
} |
|||
|
|||
public onDeleteAllActivities() { |
|||
this.deleteAllActivities.emit(); |
|||
} |
|||
|
|||
public onImport() { |
|||
this.import.emit(); |
|||
} |
|||
|
|||
public onImportDividends() { |
|||
this.importDividends.emit(); |
|||
} |
|||
|
|||
public onOpenComment(aComment: string) { |
|||
alert(aComment); |
|||
} |
|||
|
|||
public onOpenPositionDialog({ dataSource, symbol }: UniqueAsset): void { |
|||
this.router.navigate([], { |
|||
queryParams: { dataSource, symbol, positionDetailDialog: true } |
|||
}); |
|||
} |
|||
|
|||
public onUpdateActivity(aActivity: OrderWithAccount) { |
|||
this.activityToUpdate.emit(aActivity); |
|||
} |
|||
|
|||
public toggleAllRows() { |
|||
this.areAllRowsSelected() |
|||
? this.selectedRows.clear() |
|||
: this.dataSource.data.forEach((row) => this.selectedRows.select(row)); |
|||
|
|||
this.selectedActivities.emit(this.selectedRows.selected); |
|||
} |
|||
|
|||
public ngOnDestroy() { |
|||
this.unsubscribeSubject.next(); |
|||
this.unsubscribeSubject.complete(); |
|||
} |
|||
} |
@ -0,0 +1,42 @@ |
|||
import { CommonModule } from '@angular/common'; |
|||
import { CUSTOM_ELEMENTS_SCHEMA, NgModule } from '@angular/core'; |
|||
import { MatButtonModule } from '@angular/material/button'; |
|||
import { MatCheckboxModule } from '@angular/material/checkbox'; |
|||
import { MatMenuModule } from '@angular/material/menu'; |
|||
import { MatPaginatorModule } from '@angular/material/paginator'; |
|||
import { MatSortModule } from '@angular/material/sort'; |
|||
import { MatTableModule } from '@angular/material/table'; |
|||
import { MatTooltipModule } from '@angular/material/tooltip'; |
|||
import { RouterModule } from '@angular/router'; |
|||
import { GfSymbolIconModule } from '@ghostfolio/client/components/symbol-icon/symbol-icon.module'; |
|||
import { GfSymbolModule } from '@ghostfolio/client/pipes/symbol/symbol.module'; |
|||
import { GfActivityTypeModule } from '@ghostfolio/ui/activity-type'; |
|||
import { GfNoTransactionsInfoModule } from '@ghostfolio/ui/no-transactions-info'; |
|||
import { GfValueModule } from '@ghostfolio/ui/value'; |
|||
import { NgxSkeletonLoaderModule } from 'ngx-skeleton-loader'; |
|||
|
|||
import { ActivitiesTableLazyComponent } from './activities-table-lazy.component'; |
|||
|
|||
@NgModule({ |
|||
declarations: [ActivitiesTableLazyComponent], |
|||
exports: [ActivitiesTableLazyComponent], |
|||
imports: [ |
|||
CommonModule, |
|||
GfActivityTypeModule, |
|||
GfNoTransactionsInfoModule, |
|||
GfSymbolIconModule, |
|||
GfSymbolModule, |
|||
GfValueModule, |
|||
MatButtonModule, |
|||
MatCheckboxModule, |
|||
MatMenuModule, |
|||
MatPaginatorModule, |
|||
MatSortModule, |
|||
MatTableModule, |
|||
MatTooltipModule, |
|||
NgxSkeletonLoaderModule, |
|||
RouterModule |
|||
], |
|||
schemas: [CUSTOM_ELEMENTS_SCHEMA] |
|||
}) |
|||
export class GfActivitiesTableLazyModule {} |
Some files were not shown because too many files changed in this diff
Loading…
Reference in new issue