Browse Source

Export draft activities as ICS

pull/830/head
Thomas 3 years ago
parent
commit
a0de8410cc
  1. 4
      apps/api/src/app/export/export.service.ts
  2. 3
      apps/client/src/app/components/position/position-detail-dialog/position-detail-dialog.component.ts
  3. 73
      apps/client/src/app/pages/portfolio/transactions/transactions-page.component.ts
  4. 1
      apps/client/src/app/pages/portfolio/transactions/transactions-page.html
  5. 11
      libs/common/src/lib/helper.ts
  6. 11
      libs/common/src/lib/interfaces/export.interface.ts
  7. 11
      libs/ui/src/lib/activities-table/activities-table.component.html
  8. 17
      libs/ui/src/lib/activities-table/activities-table.component.ts

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

@ -42,6 +42,7 @@ export class ExportService {
accountId, accountId,
date, date,
fee, fee,
id,
quantity, quantity,
SymbolProfile, SymbolProfile,
type, type,
@ -49,13 +50,14 @@ export class ExportService {
}) => { }) => {
return { return {
accountId, accountId,
date,
fee, fee,
id,
quantity, quantity,
type, type,
unitPrice, unitPrice,
currency: SymbolProfile.currency, currency: SymbolProfile.currency,
dataSource: SymbolProfile.dataSource, dataSource: SymbolProfile.dataSource,
date: date.toISOString(),
symbol: type === 'ITEM' ? SymbolProfile.name : SymbolProfile.symbol symbol: type === 'ITEM' ? SymbolProfile.name : SymbolProfile.symbol
}; };
} }

3
apps/client/src/app/components/position/position-detail-dialog/position-detail-dialog.component.ts

@ -217,7 +217,8 @@ export class PositionDetailDialog implements OnDestroy, OnInit {
parseISO(data.meta.date), parseISO(data.meta.date),
'yyyyMMddHHmm' 'yyyyMMddHHmm'
)}.json`, )}.json`,
'text/plain' 'text/plain',
'json'
); );
}); });
} }

73
apps/client/src/app/pages/portfolio/transactions/transactions-page.component.ts

@ -11,11 +11,11 @@ import { ImpersonationStorageService } from '@ghostfolio/client/services/imperso
import { ImportTransactionsService } from '@ghostfolio/client/services/import-transactions.service'; import { ImportTransactionsService } from '@ghostfolio/client/services/import-transactions.service';
import { UserService } from '@ghostfolio/client/services/user/user.service'; import { UserService } from '@ghostfolio/client/services/user/user.service';
import { downloadAsFile } from '@ghostfolio/common/helper'; import { downloadAsFile } from '@ghostfolio/common/helper';
import { User } from '@ghostfolio/common/interfaces'; import { Export, User } from '@ghostfolio/common/interfaces';
import { hasPermission, permissions } from '@ghostfolio/common/permissions'; import { hasPermission, permissions } from '@ghostfolio/common/permissions';
import { DataSource, Order as OrderModel } from '@prisma/client'; import { DataSource, Order as OrderModel, Type } from '@prisma/client';
import { format, parseISO } from 'date-fns'; import { format, parseISO } from 'date-fns';
import { isArray } from 'lodash'; import { capitalize, isArray } from 'lodash';
import { DeviceDetectorService } from 'ngx-device-detector'; import { DeviceDetectorService } from 'ngx-device-detector';
import { Subject, Subscription } from 'rxjs'; import { Subject, Subscription } from 'rxjs';
import { takeUntil } from 'rxjs/operators'; import { takeUntil } from 'rxjs/operators';
@ -152,13 +152,35 @@ export class TransactionsPageComponent implements OnDestroy, OnInit {
.fetchExport(activityIds) .fetchExport(activityIds)
.pipe(takeUntil(this.unsubscribeSubject)) .pipe(takeUntil(this.unsubscribeSubject))
.subscribe((data) => { .subscribe((data) => {
for (const activity of data.activities) {
delete activity.id;
}
downloadAsFile( downloadAsFile(
data, data,
`ghostfolio-export-${format( `ghostfolio-export-${format(
parseISO(data.meta.date), parseISO(data.meta.date),
'yyyyMMddHHmm' 'yyyyMMddHHmm'
)}.json`, )}.json`,
'text/plain' 'text/plain',
'json'
);
});
}
public onExportDrafts(activityIds?: string[]) {
this.dataService
.fetchExport(activityIds)
.pipe(takeUntil(this.unsubscribeSubject))
.subscribe((data) => {
downloadAsFile(
this.getIcsContent(data.activities),
`ghostfolio-drafts-${format(
parseISO(data.meta.date),
'yyyyMMddHHmm'
)}.ics`,
'text/plain',
'string'
); );
}); });
} }
@ -293,6 +315,49 @@ export class TransactionsPageComponent implements OnDestroy, OnInit {
this.unsubscribeSubject.complete(); this.unsubscribeSubject.complete();
} }
private getIcsContent(aActivities: Export['activities']) {
const header = [
'BEGIN:VCALENDAR',
'VERSION:2.0',
'PRODID:-//Ghostfolio//NONSGML v1.0//EN'
];
const events = aActivities.map((activity) => {
return this.getEvent({
date: parseISO(activity.date),
id: activity.id,
symbol: activity.symbol,
type: activity.type
});
});
const footer = ['END:VCALENDAR'];
return [...header, ...events, ...footer].join('\n');
}
private getEvent({
date,
id,
symbol,
type
}: {
date: Date;
id: string;
symbol: string;
type: Type;
}) {
const today = format(new Date(), 'yyyyMMdd');
return [
'BEGIN:VEVENT',
`UID:${id}`,
`DTSTAMP:${today}T000000`,
`DTSTART;VALUE=DATE:${format(date, 'yyyyMMdd')}`,
`DTEND;VALUE=DATE:${format(date, 'yyyyMMdd')}`,
`SUMMARY:${capitalize(type)} ${symbol}`,
'END:VEVENT'
].join('\n');
}
private handleImportError({ private handleImportError({
activities, activities,
error error

1
apps/client/src/app/pages/portfolio/transactions/transactions-page.html

@ -15,6 +15,7 @@
(activityToClone)="onCloneTransaction($event)" (activityToClone)="onCloneTransaction($event)"
(activityToUpdate)="onUpdateTransaction($event)" (activityToUpdate)="onUpdateTransaction($event)"
(export)="onExport($event)" (export)="onExport($event)"
(exportDrafts)="onExportDrafts($event)"
(import)="onImport()" (import)="onImport()"
></gf-activities-table> ></gf-activities-table>
</div> </div>

11
libs/common/src/lib/helper.ts

@ -15,10 +15,17 @@ export function decodeDataSource(encodedDataSource: string) {
export function downloadAsFile( export function downloadAsFile(
aContent: unknown, aContent: unknown,
aFileName: string, aFileName: string,
aContentType: string aContentType: string,
aType: 'json' | 'string'
) { ) {
const a = document.createElement('a'); const a = document.createElement('a');
const file = new Blob([JSON.stringify(aContent, undefined, ' ')], { let content = aContent;
if (aType === 'json') {
content = JSON.stringify(aContent, undefined, ' ');
}
const file = new Blob([<string>content], {
type: aContentType type: aContentType
}); });
a.href = URL.createObjectURL(file); a.href = URL.createObjectURL(file);

11
libs/common/src/lib/interfaces/export.interface.ts

@ -5,5 +5,14 @@ export interface Export {
date: string; date: string;
version: string; version: string;
}; };
activities: Partial<Order>[]; activities: (Omit<
Order,
| 'accountUserId'
| 'createdAt'
| 'date'
| 'isDraft'
| 'symbolProfileId'
| 'updatedAt'
| 'userId'
> & { date: string; symbol: string })[];
} }

11
libs/ui/src/lib/activities-table/activities-table.component.html

@ -356,11 +356,22 @@
*ngIf="hasPermissionToExportActivities" *ngIf="hasPermissionToExportActivities"
class="align-items-center d-flex" class="align-items-center d-flex"
mat-menu-item mat-menu-item
[disabled]="dataSource.data.length === 0"
(click)="onExport()" (click)="onExport()"
> >
<ion-icon class="mr-2" name="cloud-download-outline"></ion-icon> <ion-icon class="mr-2" name="cloud-download-outline"></ion-icon>
<span i18n>Export</span> <span i18n>Export</span>
</button> </button>
<button
*ngIf="hasPermissionToExportActivities"
class="align-items-center d-flex"
mat-menu-item
[disabled]="!hasDrafts"
(click)="onExportDrafts()"
>
<ion-icon class="mr-2" name="calendar-clear-outline"></ion-icon>
<span i18n>Export Drafts as ICS</span>
</button>
</mat-menu> </mat-menu>
</th> </th>
<td *matCellDef="let element" class="px-1 text-center" mat-cell> <td *matCellDef="let element" class="px-1 text-center" mat-cell>

17
libs/ui/src/lib/activities-table/activities-table.component.ts

@ -56,6 +56,7 @@ export class ActivitiesTableComponent implements OnChanges, OnDestroy {
@Output() activityToClone = new EventEmitter<OrderWithAccount>(); @Output() activityToClone = new EventEmitter<OrderWithAccount>();
@Output() activityToUpdate = new EventEmitter<OrderWithAccount>(); @Output() activityToUpdate = new EventEmitter<OrderWithAccount>();
@Output() export = new EventEmitter<string[]>(); @Output() export = new EventEmitter<string[]>();
@Output() exportDrafts = new EventEmitter<string[]>();
@Output() import = new EventEmitter<void>(); @Output() import = new EventEmitter<void>();
@ViewChild('autocomplete') matAutocomplete: MatAutocomplete; @ViewChild('autocomplete') matAutocomplete: MatAutocomplete;
@ -68,6 +69,7 @@ export class ActivitiesTableComponent implements OnChanges, OnDestroy {
public endOfToday = endOfToday(); public endOfToday = endOfToday();
public filters$: Subject<string[]> = new BehaviorSubject([]); public filters$: Subject<string[]> = new BehaviorSubject([]);
public filters: Observable<string[]> = this.filters$.asObservable(); public filters: Observable<string[]> = this.filters$.asObservable();
public hasDrafts = false;
public isAfter = isAfter; public isAfter = isAfter;
public isLoading = true; public isLoading = true;
public isUUID = isUUID; public isUUID = isUUID;
@ -198,6 +200,18 @@ export class ActivitiesTableComponent implements OnChanges, OnDestroy {
} }
} }
public onExportDrafts() {
this.exportDrafts.emit(
this.dataSource.filteredData
.filter((activity) => {
return activity.isDraft;
})
.map((activity) => {
return activity.id;
})
);
}
public onImport() { public onImport() {
this.import.emit(); this.import.emit();
} }
@ -234,6 +248,9 @@ export class ActivitiesTableComponent implements OnChanges, OnDestroy {
this.filters$.next(this.allFilters); this.filters$.next(this.allFilters);
this.hasDrafts = this.dataSource.data.some((activity) => {
return activity.isDraft === true;
});
this.totalFees = this.getTotalFees(); this.totalFees = this.getTotalFees();
this.totalValue = this.getTotalValue(); this.totalValue = this.getTotalValue();
} }

Loading…
Cancel
Save