Browse Source

Merge branch 'feature/add-accounts-import-export' of https://github.com/yksolanki9/ghostfolio into feature/add-accounts-import-export

pull/1635/head
yksolanki9 3 years ago
parent
commit
17a78a81a4
  1. 13
      CHANGELOG.md
  2. 3
      apps/api/src/app/export/export.service.ts
  3. 4
      apps/api/src/app/import/import.controller.ts
  4. 22
      apps/api/src/app/import/import.service.ts
  5. 14
      apps/client/src/app/components/admin-market-data-detail/market-data-detail-dialog/market-data-detail-dialog.html
  6. 6
      apps/client/src/app/components/admin-market-data-detail/market-data-detail-dialog/market-data-detail-dialog.module.ts
  7. 16
      apps/client/src/app/components/admin-market-data-detail/market-data-detail-dialog/market-data-detail-dialog.scss
  8. 2
      apps/client/src/app/pages/portfolio/activities/activities-page.html
  9. 4
      apps/client/src/app/pages/portfolio/activities/activities-page.module.ts
  10. 2
      apps/client/src/app/pages/portfolio/activities/create-or-update-activity-dialog/create-or-update-activity-dialog.component.ts
  11. 28
      apps/client/src/app/pages/portfolio/activities/create-or-update-activity-dialog/create-or-update-activity-dialog.html
  12. 14
      apps/client/src/app/pages/portfolio/activities/create-or-update-activity-dialog/create-or-update-activity-dialog.module.ts
  13. 35
      apps/client/src/app/pages/portfolio/activities/create-or-update-activity-dialog/create-or-update-activity-dialog.scss
  14. 48
      test/import/ok-without-accounts.json
  15. 21
      test/import/ok.json

13
CHANGELOG.md

@ -5,6 +5,19 @@ All notable changes to this project will be documented in this file.
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
## Unreleased
### Added
- Added support to export accounts
- Added suport to import accounts
### Changed
- Migrated the style of `ActivitiesPageModule` to `@angular/material` `15` (mdc)
- Migrated the style of `GfCreateOrUpdateActivityDialogModule` to `@angular/material` `15` (mdc)
- Migrated the style of `GfMarketDataDetailDialogModule` to `@angular/material` `15` (mdc)
## 1.231.0 - 2023-02-04
### Added

3
apps/api/src/app/export/export.service.ts

@ -15,6 +15,9 @@ export class ExportService {
userId: string;
}): Promise<Export> {
const accounts = await this.prismaService.account.findMany({
orderBy: {
name: 'asc'
},
select: {
accountType: true,
balance: true,

4
apps/api/src/app/import/import.controller.ts

@ -68,11 +68,11 @@ export class ImportController {
try {
const activities = await this.importService.import({
accountsDto: importData.accounts || [],
activitiesDto: importData.activities,
isDryRun,
maxActivitiesToImport,
userCurrency,
accountsDto: importData.accounts ?? [],
activitiesDto: importData.activities,
userId: this.request.user.id
});

22
apps/api/src/app/import/import.service.ts

@ -120,20 +120,22 @@ export class ImportService {
const existingAccounts = await this.accountService.accounts({
where: {
id: {
in: accountsDto.map((account) => account.id)
in: accountsDto.map(({ id }) => {
return id;
})
}
}
});
//Create new accounts during dryRun so that new account IDs don't get invalidated
// Create new accounts during dryRun so that new account IDs don't get invalidated
if (isDryRun && accountsDto?.length) {
for (let account of accountsDto) {
//Check if there is any existing account with the same ID
for (const account of accountsDto) {
// Check if there is any existing account with the same ID
const accountWithSameId = existingAccounts.find(
(existingAccount) => existingAccount.id === account.id
);
//If there is no account or if the account belongs to a different user then create a new account
// If there is no account or if the account belongs to a different user then create a new account
if (!accountWithSameId || accountWithSameId.userId !== userId) {
let oldAccountId: string;
const platformId = account.platformId;
@ -145,23 +147,23 @@ export class ImportService {
delete account.id;
}
const newAccountObj = {
const newAccountObject = {
...account,
User: { connect: { id: userId } }
};
if (platformId) {
Object.assign(newAccountObj, {
Object.assign(newAccountObject, {
Platform: { connect: { id: platformId } }
});
}
const newAccount = await this.accountService.createAccount(
newAccountObj,
newAccountObject,
userId
);
//Store the new to old account ID mappings for updating activities
// Store the new to old account ID mappings for updating activities
if (accountWithSameId && oldAccountId) {
accountIdMapping[oldAccountId] = newAccount.id;
}
@ -178,7 +180,7 @@ export class ImportService {
}
}
//If a new account is created, then update the accountId in all activities
// If a new account is created, then update the accountId in all activities
if (isDryRun) {
if (Object.keys(accountIdMapping).includes(activity.accountId)) {
activity.accountId = accountIdMapping[activity.accountId];

14
apps/client/src/app/components/admin-market-data-detail/market-data-detail-dialog/market-data-detail-dialog.html

@ -1,6 +1,6 @@
<form class="d-flex flex-column h-100">
<h1 i18n mat-dialog-title>Details for {{ data.symbol }}</h1>
<div class="flex-grow-1" mat-dialog-content>
<div class="flex-grow-1 pt-3" mat-dialog-content>
<div class="mb-3">
<mat-form-field appearance="outline" class="w-100">
<mat-label i18n>Date</mat-label>
@ -11,7 +11,7 @@
[matDatepicker]="date"
[(ngModel)]="data.date"
/>
<mat-datepicker-toggle matSuffix [for]="date">
<mat-datepicker-toggle class="mr-2" matSuffix [for]="date">
<ion-icon
class="text-muted"
matDatepickerToggleIcon
@ -21,7 +21,7 @@
<mat-datepicker #date disabled="true"></mat-datepicker>
</mat-form-field>
</div>
<div>
<div class="align-items-start d-flex">
<mat-form-field appearance="outline" class="w-100">
<mat-label i18n>Market Price</mat-label>
<input
@ -30,16 +30,16 @@
type="number"
[(ngModel)]="data.marketPrice"
/>
<span class="ml-2" matSuffix>{{ data.currency }}</span>
<span class="ml-2" matTextSuffix>{{ data.currency }}</span>
</mat-form-field>
<button
mat-icon-button
matSuffix
class="apply-current-market-price ml-2 no-min-width"
mat-button
title="Fetch market price"
(click)="onFetchSymbolForDate()"
>
<ion-icon class="text-muted" name="refresh-outline"></ion-icon>
</button>
</mat-form-field>
</div>
</div>
<div class="justify-content-end" mat-dialog-actions>

6
apps/client/src/app/components/admin-market-data-detail/market-data-detail-dialog/market-data-detail-dialog.module.ts

@ -2,10 +2,10 @@ import { CommonModule } from '@angular/common';
import { CUSTOM_ELEMENTS_SCHEMA, NgModule } from '@angular/core';
import { FormsModule, ReactiveFormsModule } from '@angular/forms';
import { MatDatepickerModule } from '@angular/material/datepicker';
import { MatLegacyButtonModule as MatButtonModule } from '@angular/material/legacy-button';
import { MatButtonModule } from '@angular/material/button';
import { MatLegacyDialogModule as MatDialogModule } from '@angular/material/legacy-dialog';
import { MatLegacyFormFieldModule as MatFormFieldModule } from '@angular/material/legacy-form-field';
import { MatLegacyInputModule as MatInputModule } from '@angular/material/legacy-input';
import { MatFormFieldModule } from '@angular/material/form-field';
import { MatInputModule } from '@angular/material/input';
import { MarketDataDetailDialog } from './market-data-detail-dialog.component';

16
apps/client/src/app/components/admin-market-data-detail/market-data-detail-dialog/market-data-detail-dialog.scss

@ -4,19 +4,9 @@
.mat-dialog-content {
max-height: unset;
.mat-form-field-appearance-outline {
::ng-deep {
.mat-form-field-suffix {
top: -0.3rem;
}
.mat-form-field-wrapper {
padding-bottom: 0;
}
}
ion-icon {
font-size: 130%;
.mat-mdc-button {
&.apply-current-market-price {
height: 56px;
}
}
}

2
apps/client/src/app/pages/portfolio/activities/activities-page.html

@ -33,7 +33,7 @@
[queryParams]="{ createDialog: true }"
[routerLink]="[]"
>
<ion-icon name="add-outline" size="large"></ion-icon>
<ion-icon class="mt-2" name="add-outline" size="large"></ion-icon>
</a>
</div>
</div>

4
apps/client/src/app/pages/portfolio/activities/activities-page.module.ts

@ -1,7 +1,7 @@
import { CommonModule } from '@angular/common';
import { CUSTOM_ELEMENTS_SCHEMA, NgModule } from '@angular/core';
import { MatLegacyButtonModule as MatButtonModule } from '@angular/material/legacy-button';
import { MatLegacySnackBarModule as MatSnackBarModule } from '@angular/material/legacy-snack-bar';
import { MatButtonModule } from '@angular/material/button';
import { MatSnackBarModule } from '@angular/material/snack-bar';
import { RouterModule } from '@angular/router';
import { ImportActivitiesService } from '@ghostfolio/client/services/import-activities.service';
import { GfActivitiesTableModule } from '@ghostfolio/ui/activities-table/activities-table.module';

2
apps/client/src/app/pages/portfolio/activities/create-or-update-activity-dialog/create-or-update-activity-dialog.component.ts

@ -10,7 +10,7 @@ import {
} from '@angular/core';
import { FormBuilder, FormGroup, Validators } from '@angular/forms';
import { DateAdapter, MAT_DATE_LOCALE } from '@angular/material/core';
import { MatLegacyAutocompleteSelectedEvent as MatAutocompleteSelectedEvent } from '@angular/material/legacy-autocomplete';
import { MatAutocompleteSelectedEvent } from '@angular/material/autocomplete';
import {
MAT_LEGACY_DIALOG_DATA as MAT_DIALOG_DATA,
MatLegacyDialogRef as MatDialogRef

28
apps/client/src/app/pages/portfolio/activities/create-or-update-activity-dialog/create-or-update-activity-dialog.html

@ -6,7 +6,7 @@
>
<h1 *ngIf="data.activity.id" i18n mat-dialog-title>Update activity</h1>
<h1 *ngIf="!data.activity.id" i18n mat-dialog-title>Add activity</h1>
<div class="flex-grow-1" mat-dialog-content>
<div class="flex-grow-1 pt-3" mat-dialog-content>
<div>
<mat-form-field appearance="outline" class="w-100">
<mat-label i18n>Type</mat-label>
@ -76,7 +76,7 @@
<div class="d-none">
<mat-form-field appearance="outline" class="w-100">
<mat-label i18n>Currency</mat-label>
<mat-select class="no-arrow" formControlName="currency">
<mat-select formControlName="currency">
<mat-option *ngFor="let currency of currencies" [value]="currency"
>{{ currency }}</mat-option
>
@ -93,7 +93,7 @@
<mat-form-field appearance="outline" class="w-100">
<mat-label i18n>Date</mat-label>
<input formControlName="date" matInput [matDatepicker]="date" />
<mat-datepicker-toggle matSuffix [for]="date">
<mat-datepicker-toggle class="mr-2" matSuffix [for]="date">
<ion-icon
class="text-muted"
matDatepickerToggleIcon
@ -109,7 +109,7 @@
<input formControlName="quantity" matInput type="number" />
</mat-form-field>
</div>
<div>
<div class="align-items-start d-flex">
<mat-form-field appearance="outline" class="w-100">
<mat-label
><ng-container [ngSwitch]="activityForm.controls['type']?.value">
@ -121,20 +121,20 @@
</ng-container>
</mat-label>
<input formControlName="unitPrice" matInput type="number" />
<span class="ml-2" matSuffix
<span class="ml-2" matTextSuffix
>{{ activityForm.controls['currency'].value }}</span
>
</mat-form-field>
<button
*ngIf="currentMarketPrice && (data.activity.type === 'BUY' || data.activity.type === 'SELL')"
mat-icon-button
matSuffix
class="apply-current-market-price ml-2 no-min-width"
mat-button
title="Apply current market price"
type="button"
(click)="applyCurrentMarketPrice()"
>
<ion-icon class="text-muted" name="refresh-outline"></ion-icon>
</button>
</mat-form-field>
</div>
<div>
<mat-form-field appearance="outline" class="w-100">
@ -142,7 +142,7 @@
<input formControlName="feeInCustomCurrency" matInput type="number" />
<div
class="ml-2"
matSuffix
matTextSuffix
[ngClass]="{ 'd-none': !activityForm.controls['currency']?.value }"
>
<mat-select formControlName="currencyOfFee">
@ -157,7 +157,7 @@
<mat-form-field appearance="outline" class="w-100">
<mat-label i18n>Fee</mat-label>
<input formControlName="fee" matInput type="number" />
<span class="ml-2" matSuffix
<span class="ml-2" matTextSuffix
>{{ activityForm.controls['currency'].value }}</span
>
</mat-form-field>
@ -207,8 +207,8 @@
<div [ngClass]="{ 'd-none': tags?.length <= 0 }">
<mat-form-field appearance="outline" class="w-100">
<mat-label i18n>Tags</mat-label>
<mat-chip-list #tagsChipList>
<mat-chip
<mat-chip-grid #tagsChipList>
<mat-chip-row
*ngFor="let tag of activityForm.controls['tags']?.value"
matChipRemove
[removable]="true"
@ -216,7 +216,7 @@
>
{{ tag.name }}
<ion-icon class="ml-2" matPrefix name="close-outline"></ion-icon>
</mat-chip>
</mat-chip-row>
<input
#tagInput
name="close-outline"
@ -224,7 +224,7 @@
[matChipInputFor]="tagsChipList"
[matChipInputSeparatorKeyCodes]="separatorKeysCodes"
/>
</mat-chip-list>
</mat-chip-grid>
<mat-autocomplete
#autocompleteTags="matAutocomplete"
(optionSelected)="onAddTag($event)"

14
apps/client/src/app/pages/portfolio/activities/create-or-update-activity-dialog/create-or-update-activity-dialog.module.ts

@ -2,14 +2,14 @@ import { CommonModule } from '@angular/common';
import { CUSTOM_ELEMENTS_SCHEMA, NgModule } from '@angular/core';
import { FormsModule, ReactiveFormsModule } from '@angular/forms';
import { MatDatepickerModule } from '@angular/material/datepicker';
import { MatLegacyAutocompleteModule as MatAutocompleteModule } from '@angular/material/legacy-autocomplete';
import { MatLegacyButtonModule as MatButtonModule } from '@angular/material/legacy-button';
import { MatLegacyChipsModule as MatChipsModule } from '@angular/material/legacy-chips';
import { MatAutocompleteModule } from '@angular/material/autocomplete';
import { MatButtonModule } from '@angular/material/button';
import { MatChipsModule } from '@angular/material/chips';
import { MatLegacyDialogModule as MatDialogModule } from '@angular/material/legacy-dialog';
import { MatLegacyFormFieldModule as MatFormFieldModule } from '@angular/material/legacy-form-field';
import { MatLegacyInputModule as MatInputModule } from '@angular/material/legacy-input';
import { MatLegacyProgressSpinnerModule as MatProgressSpinnerModule } from '@angular/material/legacy-progress-spinner';
import { MatLegacySelectModule as MatSelectModule } from '@angular/material/legacy-select';
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 { GfValueModule } from '@ghostfolio/ui/value';

35
apps/client/src/app/pages/portfolio/activities/create-or-update-activity-dialog/create-or-update-activity-dialog.scss

@ -20,36 +20,15 @@
}
}
.mat-chip {
cursor: pointer;
min-height: 1.5rem !important;
}
.mat-form-field-appearance-outline {
::ng-deep {
.mat-form-field-suffix {
top: -0.3rem;
}
}
ion-icon {
font-size: 130%;
}
}
.mat-select {
&.no-arrow {
::ng-deep {
.mat-select-arrow {
opacity: 0;
}
}
.mat-datepicker-input {
&.mat-mdc-input-element:disabled {
color: var(--dark-primary-text);
}
}
.mat-datepicker-input {
&.mat-input-element:disabled {
color: var(--dark-primary-text);
.mat-mdc-button {
&.apply-current-market-price {
height: 56px;
}
}
}
@ -58,7 +37,7 @@
:host-context(.is-dark-theme) {
.mat-dialog-content {
.mat-datepicker-input {
&.mat-input-element:disabled {
&.mat-mdc-input-element:disabled {
color: var(--light-primary-text);
}
}

48
test/import/ok-without-accounts.json

@ -0,0 +1,48 @@
{
"meta": {
"date": "2022-04-01T00:00:00.000Z",
"version": "dev"
},
"activities": [
{
"fee": 0,
"quantity": 0,
"type": "BUY",
"unitPrice": 0,
"currency": "USD",
"dataSource": "YAHOO",
"date": "2050-06-05T22:00:00.000Z",
"symbol": "MSFT"
},
{
"fee": 0,
"quantity": 1,
"type": "ITEM",
"unitPrice": 500000,
"currency": "USD",
"dataSource": "MANUAL",
"date": "2021-12-31T22:00:00.000Z",
"symbol": "Penthouse Apartment"
},
{
"fee": 0,
"quantity": 5,
"type": "DIVIDEND",
"unitPrice": 0.62,
"currency": "USD",
"dataSource": "YAHOO",
"date": "2021-11-16T22:00:00.000Z",
"symbol": "MSFT"
},
{
"fee": 19,
"quantity": 5,
"type": "BUY",
"unitPrice": 298.58,
"currency": "USD",
"dataSource": "YAHOO",
"date": "2021-09-15T22:00:00.000Z",
"symbol": "MSFT"
}
]
}

21
test/import/ok.json

@ -1,10 +1,23 @@
{
"meta": {
"date": "2022-04-01T00:00:00.000Z",
"date": "2023-02-05T00:00:00.000Z",
"version": "dev"
},
"accounts": [
{
"accountType": "SECURITIES",
"balance": 2000,
"currency": "USD",
"id": "b2d3fe1d-d6a8-41a3-be39-07ef5e9480f0",
"isExcluded": false,
"name": "My Online Trading Account",
"platformId": null
}
],
"activities": [
{
"accountId": "b2d3fe1d-d6a8-41a3-be39-07ef5e9480f0",
"comment": null,
"fee": 0,
"quantity": 0,
"type": "BUY",
@ -15,6 +28,8 @@
"symbol": "MSFT"
},
{
"accountId": null,
"comment": null,
"fee": 0,
"quantity": 1,
"type": "ITEM",
@ -25,6 +40,8 @@
"symbol": "Penthouse Apartment"
},
{
"accountId": "b2d3fe1d-d6a8-41a3-be39-07ef5e9480f0",
"comment": null,
"fee": 0,
"quantity": 5,
"type": "DIVIDEND",
@ -35,6 +52,8 @@
"symbol": "MSFT"
},
{
"accountId": "b2d3fe1d-d6a8-41a3-be39-07ef5e9480f0",
"comment": "My first order",
"fee": 19,
"quantity": 5,
"type": "BUY",

Loading…
Cancel
Save