From bd1233b0e2c4b7b506a54be0f1bed60db51d282a Mon Sep 17 00:00:00 2001
From: ksyasuda <ksyasuda@umich.edu>
Date: Thu, 8 Aug 2024 19:35:20 -0700
Subject: [PATCH] Add expand/collapse for Top/Bottom secion off portfolio
 analysis page

- Add button to expand/collapse section
    - Show all positions when expanded
- Add pagination with 10 items per page
---
 .gitignore                                    |  2 +
 .../analysis/analysis-page.component.ts       | 40 +++++++++++++++--
 .../portfolio/analysis/analysis-page.html     | 44 ++++++++++++++++---
 .../analysis/analysis-page.module.ts          |  4 ++
 .../portfolio/analysis/analysis-page.scss     |  4 ++
 5 files changed, 84 insertions(+), 10 deletions(-)

diff --git a/.gitignore b/.gitignore
index d7e5e5eb..83a78305 100644
--- a/.gitignore
+++ b/.gitignore
@@ -1,5 +1,7 @@
 # See http://help.github.com/ignore-files/ for more about ignoring files.
 
+sync.sh
+
 # compiled output
 /out-tsc
 /tmp
diff --git a/apps/client/src/app/pages/portfolio/analysis/analysis-page.component.ts b/apps/client/src/app/pages/portfolio/analysis/analysis-page.component.ts
index 0450e32a..3b3a7894 100644
--- a/apps/client/src/app/pages/portfolio/analysis/analysis-page.component.ts
+++ b/apps/client/src/app/pages/portfolio/analysis/analysis-page.component.ts
@@ -2,6 +2,7 @@ import { ToggleComponent } from '@ghostfolio/client/components/toggle/toggle.com
 import { DataService } from '@ghostfolio/client/services/data.service';
 import { ImpersonationStorageService } from '@ghostfolio/client/services/impersonation-storage.service';
 import { UserService } from '@ghostfolio/client/services/user/user.service';
+import { MatPaginator, PageEvent } from '@angular/material/paginator';
 import {
   HistoricalDataItem,
   PortfolioInvestments,
@@ -31,6 +32,7 @@ export class AnalysisPageComponent implements OnDestroy, OnInit {
   public benchmarkDataItems: HistoricalDataItem[] = [];
   public benchmarks: Partial<SymbolProfile>[];
   public bottom3: PortfolioPosition[];
+  public bottomx: PortfolioPosition[];
   public dateRangeOptions = ToggleComponent.DEFAULT_DATE_RANGE_OPTIONS;
   public daysInMarket: number;
   public deviceType: string;
@@ -56,9 +58,17 @@ export class AnalysisPageComponent implements OnDestroy, OnInit {
   public portfolioEvolutionDataLabel = $localize`Investment`;
   public streaks: PortfolioInvestments['streaks'];
   public top3: PortfolioPosition[];
+  public topx: PortfolioPosition[];
+  public positions: PortfolioPosition[];
+  public positionsReversed: PortfolioPosition[];
   public unitCurrentStreak: string;
   public unitLongestStreak: string;
   public user: User;
+  public showAllTop: boolean = false;
+  public showAllBottom: boolean = false;
+  public pageSize: number = 10;
+  public pageIndexTop: number = 0;
+  public pageIndexBottom: number = 0;
 
   private unsubscribeSubject = new Subject<void>();
 
@@ -73,6 +83,25 @@ export class AnalysisPageComponent implements OnDestroy, OnInit {
     this.benchmarks = benchmarks;
   }
 
+  public pageChanged(event: PageEvent, section: 'top' | 'bottom') {
+    const newPageIndex = event.pageIndex;
+    if (section === 'top') {
+      this.pageIndexTop = newPageIndex;
+    } else if (section === 'bottom') {
+      this.pageIndexBottom = newPageIndex;
+    }
+  }
+
+  public toggleList(l: string) {
+    if (l === 'top') {
+      this.showAllTop = !this.showAllTop;
+      this.pageIndexTop = 0;
+    } else if (l === 'bottom') {
+      this.showAllBottom = !this.showAllBottom;
+      this.pageIndexBottom = 0;
+    }
+  }
+
   get savingsRate() {
     const savingsRatePerMonth =
       this.hasImpersonationId || this.user.settings.isRestrictedView
@@ -257,14 +286,17 @@ export class AnalysisPageComponent implements OnDestroy, OnInit {
           'netPerformancePercentWithCurrencyEffect'
         ).reverse();
 
-        this.top3 = holdingsSorted.slice(0, 3);
+        this.topx = holdingsSorted.slice(0, 5);
 
-        if (holdings?.length > 3) {
-          this.bottom3 = holdingsSorted.slice(-3).reverse();
+        if (holdings?.length > 5) {
+          this.bottomx = holdingsSorted.slice(-5).reverse();
         } else {
-          this.bottom3 = [];
+          this.bottomx = [];
         }
 
+        this.positions = [...holdingsSorted]
+        this.positionsReversed = [...holdingsSorted].reverse();
+
         this.changeDetectorRef.markForCheck();
       });
 
diff --git a/apps/client/src/app/pages/portfolio/analysis/analysis-page.html b/apps/client/src/app/pages/portfolio/analysis/analysis-page.html
index 970a89f7..66f8bf8a 100644
--- a/apps/client/src/app/pages/portfolio/analysis/analysis-page.html
+++ b/apps/client/src/app/pages/portfolio/analysis/analysis-page.html
@@ -168,7 +168,7 @@
         </mat-card-header>
         <mat-card-content>
           <ol class="mb-0 ml-1 pl-3">
-            @for (holding of top3; track holding) {
+            <div *ngFor="let holding of (showAllTop ?  positions.slice(pageIndexTop * pageSize, (pageIndexTop + 1) * pageSize) : positions | slice:0:5); track holding">
               <li class="py-1">
                 <a
                   class="d-flex"
@@ -192,10 +192,10 @@
                   </div>
                 </a>
               </li>
-            }
+            </div>
           </ol>
           <div>
-            @if (!top3) {
+            @if (!topx) {
               <ngx-skeleton-loader
                 animation="pulse"
                 [theme]="{
@@ -206,7 +206,23 @@
             }
           </div>
         </mat-card-content>
+        <mat-paginator
+          *ngIf="showAllTop && positions.length > 10"
+          [length]="positions.length"
+          [pageSize]="pageSize"
+          [pageSizeOptions]="[10, 20, 30, 40]"
+          (page)="pageChanged($event, 'top')"
+        >
+        </mat-paginator>
       </mat-card>
+      <button
+        class="toggle-performance-list"
+        color="{{showAllTop ? 'accent' : 'primary'}}"
+        mat-button
+        (click)="toggleList('top')"
+      >
+        {{ showAllTop ? 'Collapse' : 'Expand' }}
+      </button>
     </div>
     <div class="col-md-6">
       <mat-card appearance="outlined" class="mb-3">
@@ -217,7 +233,7 @@
         </mat-card-header>
         <mat-card-content>
           <ol class="mb-0 ml-1 pl-3">
-            @for (holding of bottom3; track holding) {
+            <div *ngFor="let holding of (showAllBottom ?  positionsReversed.slice(pageIndexBottom * pageSize, (pageIndexBottom + 1) * pageSize) : positionsReversed | slice:0:5); track holding">
               <li class="py-1">
                 <a
                   class="d-flex"
@@ -241,10 +257,10 @@
                   </div>
                 </a>
               </li>
-            }
+            </div>
           </ol>
           <div>
-            @if (!bottom3) {
+            @if (!bottomx) {
               <ngx-skeleton-loader
                 animation="pulse"
                 [theme]="{
@@ -255,7 +271,23 @@
             }
           </div>
         </mat-card-content>
+        <mat-paginator
+          *ngIf="showAllBottom && positionsReversed.length > 10"
+          [length]="positions.length"
+          [pageSize]="pageSize"
+          [pageSizeOptions]="[10, 20, 30, 40]"
+          (page)="pageChanged($event, 'bottom')"
+        >
+        </mat-paginator>
       </mat-card>
+      <button
+        class="toggle-performance-list"
+        color="{{showAllBottom ? 'accent' : 'primary'}}"
+        mat-button
+        (click)="toggleList('bottom')"
+      >
+        {{ showAllBottom ? 'Collapse' : 'Expand' }}
+      </button>
     </div>
   </div>
 
diff --git a/apps/client/src/app/pages/portfolio/analysis/analysis-page.module.ts b/apps/client/src/app/pages/portfolio/analysis/analysis-page.module.ts
index b3390545..cf0e57c1 100644
--- a/apps/client/src/app/pages/portfolio/analysis/analysis-page.module.ts
+++ b/apps/client/src/app/pages/portfolio/analysis/analysis-page.module.ts
@@ -4,6 +4,8 @@ import { GfToggleModule } from '@ghostfolio/client/components/toggle/toggle.modu
 import { GfActivitiesFilterComponent } from '@ghostfolio/ui/activities-filter';
 import { GfPremiumIndicatorComponent } from '@ghostfolio/ui/premium-indicator';
 import { GfValueComponent } from '@ghostfolio/ui/value';
+import { MatButtonModule } from '@angular/material/button';
+import { MatPaginatorModule } from '@angular/material/paginator';
 
 import { CommonModule } from '@angular/common';
 import { CUSTOM_ELEMENTS_SCHEMA, NgModule } from '@angular/core';
@@ -25,6 +27,8 @@ import { AnalysisPageComponent } from './analysis-page.component';
     GfToggleModule,
     GfValueComponent,
     MatCardModule,
+    MatButtonModule,
+    MatPaginatorModule,
     NgxSkeletonLoaderModule
   ],
   schemas: [CUSTOM_ELEMENTS_SCHEMA]
diff --git a/apps/client/src/app/pages/portfolio/analysis/analysis-page.scss b/apps/client/src/app/pages/portfolio/analysis/analysis-page.scss
index c6fb687e..01265cae 100644
--- a/apps/client/src/app/pages/portfolio/analysis/analysis-page.scss
+++ b/apps/client/src/app/pages/portfolio/analysis/analysis-page.scss
@@ -4,4 +4,8 @@
   .chart-container {
     aspect-ratio: 16 / 9;
   }
+
+  .toggle-performance-list {
+    margin-bottom: 1rem;
+  }
 }