Scouting App: View Data Interface

Add a tab to the scouting app to view all data collected by the
scouting app including notes, stats, and driver ranking.
You can sort data by ascending/descending match or team numbers.
In the future, this tab should be intergrated with the scouting
app tests to verify if data was actually submitted and a delete
option should be added so that scouts can delete and resubmit data
if necessary.

Signed-off-by: Filip Kujawa <filip.j.kujawa@gmail.com>
Change-Id: Idc8938f2c309a79f0006c6c99781ca06a506a19d
diff --git a/scouting/www/index.template.html b/scouting/www/index.template.html
index 303dca1..8fd0d7c 100644
--- a/scouting/www/index.template.html
+++ b/scouting/www/index.template.html
@@ -11,6 +11,11 @@
       integrity="sha384-1BmE4kWBq78iYhFldvKuhfTAU6auU8tT94WrHftjDbrCEXSU1oBoqyl2QvZ6jIW3"
       crossorigin="anonymous"
     />
+    <link
+      rel="stylesheet"
+      href="https://cdn.jsdelivr.net/npm/bootstrap-icons@1.10.2/font/bootstrap-icons.css"
+    />
+    <script src="https://cdn.jsdelivr.net/npm/bootstrap@5.2.3/dist/js/bootstrap.bundle.min.js"></script>
   </head>
   <body>
     <my-app></my-app>
diff --git a/scouting/www/view/BUILD b/scouting/www/view/BUILD
index fae4b23..b9b6030 100644
--- a/scouting/www/view/BUILD
+++ b/scouting/www/view/BUILD
@@ -17,8 +17,13 @@
     visibility = ["//visibility:public"],
     deps = [
         "//scouting/webserver/requests/messages:error_response_ts_fbs",
-        "//scouting/webserver/requests/messages:request_all_matches_response_ts_fbs",
-        "//scouting/webserver/requests/messages:request_all_matches_ts_fbs",
+        "//scouting/webserver/requests/messages:request_all_driver_rankings_response_ts_fbs",
+        "//scouting/webserver/requests/messages:request_all_driver_rankings_ts_fbs",
+        "//scouting/webserver/requests/messages:request_all_notes_response_ts_fbs",
+        "//scouting/webserver/requests/messages:request_all_notes_ts_fbs",
+        "//scouting/webserver/requests/messages:request_data_scouting_response_ts_fbs",
+        "//scouting/webserver/requests/messages:request_data_scouting_ts_fbs",
+        "//scouting/www/rpc",
         "@com_github_google_flatbuffers//ts:flatbuffers_ts",
         "@npm//@angular/common",
         "@npm//@angular/core",
diff --git a/scouting/www/view/view.component.ts b/scouting/www/view/view.component.ts
index 8150efd..30f5914 100644
--- a/scouting/www/view/view.component.ts
+++ b/scouting/www/view/view.component.ts
@@ -1,8 +1,189 @@
 import {Component, OnInit} from '@angular/core';
+import {
+  Ranking,
+  RequestAllDriverRankingsResponse,
+} from 'org_frc971/scouting/webserver/requests/messages/request_all_driver_rankings_response_generated';
+import {
+  Stats,
+  RequestDataScoutingResponse,
+} from 'org_frc971/scouting/webserver/requests/messages/request_data_scouting_response_generated';
+import {
+  Note,
+  RequestAllNotesResponse,
+} from 'org_frc971/scouting/webserver/requests/messages/request_all_notes_response_generated';
+
+import {ViewDataRequestor} from '../rpc/view_data_requestor';
+
+type Source = 'Notes' | 'Stats' | 'DriverRanking';
+
+//TODO(Filip): Deduplicate
+const COMP_LEVEL_LABELS = {
+  qm: 'Qualifications',
+  ef: 'Eighth Finals',
+  qf: 'Quarter Finals',
+  sf: 'Semi Finals',
+  f: 'Finals',
+};
 
 @Component({
   selector: 'app-view',
   templateUrl: './view.ng.html',
   styleUrls: ['../common.css', './view.component.css'],
 })
-export class ViewComponent {}
+export class ViewComponent {
+  constructor(private readonly viewDataRequestor: ViewDataRequestor) {}
+
+  // Make COMP_LEVEL_LABELS available in view.ng.html.
+  readonly COMP_LEVEL_LABELS = COMP_LEVEL_LABELS;
+
+  // Progress and error messages to display to
+  // the user when fetching data.
+  progressMessage: string = '';
+  errorMessage: string = '';
+
+  // The current data source being displayed.
+  currentSource: Source = 'Notes';
+
+  // Current sort (ascending/descending match numbers).
+  // noteList is sorted based on team number until match
+  // number is added for note scouting.
+  ascendingSort = true;
+
+  // Stores the corresponding data.
+  noteList: Note[] = [];
+  driverRankingList: Ranking[] = [];
+  statList: Stats[] = [];
+
+  // Fetch notes on initialization.
+  ngOnInit() {
+    this.fetchCurrentSource();
+  }
+
+  // Called when a user changes the sort direction.
+  // Changes the data order between ascending/descending.
+  sortData() {
+    this.ascendingSort = !this.ascendingSort;
+    if (!this.ascendingSort) {
+      this.driverRankingList.sort((a, b) => b.matchNumber() - a.matchNumber());
+      this.noteList.sort((a, b) => b.team() - a.team());
+      this.statList.sort((a, b) => b.match() - a.match());
+    } else {
+      this.driverRankingList.sort((a, b) => a.matchNumber() - b.matchNumber());
+      this.noteList.sort((a, b) => a.team() - b.team());
+      this.statList.sort((a, b) => a.match() - b.match());
+    }
+  }
+
+  // Called when a user selects a new data source
+  // from the dropdown.
+  switchDataSource(target: Source) {
+    this.currentSource = target;
+    this.progressMessage = '';
+    this.errorMessage = '';
+    this.noteList = [];
+    this.driverRankingList = [];
+    this.statList = [];
+    this.fetchCurrentSource();
+  }
+
+  // Call the method to fetch data for the current source.
+  fetchCurrentSource() {
+    switch (this.currentSource) {
+      case 'Notes': {
+        this.fetchNotes();
+      }
+
+      case 'Stats': {
+        this.fetchStats();
+      }
+
+      case 'DriverRanking': {
+        this.fetchDriverRanking();
+      }
+    }
+  }
+
+  // TODO(Filip): Add delete functionality.
+  // Gets called when a user clicks the delete icon.
+  async deleteData() {
+    const block_alerts = document.getElementById(
+      'block_alerts'
+    ) as HTMLInputElement;
+    if (!block_alerts.checked) {
+      if (!window.confirm('Actually delete data?')) {
+        this.errorMessage = 'Deleting data has not been implemented yet.';
+        return;
+      }
+    }
+  }
+
+  // Fetch all driver ranking data and store in driverRankingList.
+  async fetchDriverRanking() {
+    this.progressMessage = 'Fetching driver ranking data. Please be patient.';
+    this.errorMessage = '';
+
+    try {
+      this.driverRankingList =
+        await this.viewDataRequestor.fetchDriverRankingList();
+      this.progressMessage = 'Successfully fetched driver ranking data.';
+    } catch (e) {
+      this.errorMessage = e;
+      this.progressMessage = '';
+    }
+  }
+
+  // Fetch all data scouting (stats) data and store in statList.
+  async fetchStats() {
+    this.progressMessage = 'Fetching stats list. Please be patient.';
+    this.errorMessage = '';
+
+    try {
+      this.statList = await this.viewDataRequestor.fetchStatsList();
+      this.progressMessage = 'Successfully fetched stats list.';
+    } catch (e) {
+      this.errorMessage = e;
+      this.progressMessage = '';
+    }
+  }
+
+  // Fetch all notes data and store in noteList.
+  async fetchNotes() {
+    this.progressMessage = 'Fetching notes list. Please be patient.';
+    this.errorMessage = '';
+
+    try {
+      this.noteList = await this.viewDataRequestor.fetchNoteList();
+      this.progressMessage = 'Successfully fetched note list.';
+    } catch (e) {
+      this.errorMessage = e;
+      this.progressMessage = '';
+    }
+  }
+
+  // Parse all selected keywords for a note entry
+  // into one string to be displayed in the table.
+  parseKeywords(entry: Note) {
+    let parsedKeywords = '';
+
+    if (entry.goodDriving()) {
+      parsedKeywords += 'Good Driving ';
+    }
+    if (entry.badDriving()) {
+      parsedKeywords += 'Bad Driving ';
+    }
+    if (entry.solidClimb()) {
+      parsedKeywords += 'Solid Climb ';
+    }
+    if (entry.sketchyClimb()) {
+      parsedKeywords += 'Sketchy Climb ';
+    }
+    if (entry.goodDefense()) {
+      parsedKeywords += 'Good Defense ';
+    }
+    if (entry.badDefense()) {
+      parsedKeywords += 'Bad Defense ';
+    }
+
+    return parsedKeywords;
+  }
+}
diff --git a/scouting/www/view/view.ng.html b/scouting/www/view/view.ng.html
index b7ed627..c361638 100644
--- a/scouting/www/view/view.ng.html
+++ b/scouting/www/view/view.ng.html
@@ -1 +1,147 @@
 <h2>View Data</h2>
+<!-- Drop down to select data type. -->
+<div class="dropdown">
+  <button
+    class="btn btn-secondary dropdown-toggle"
+    type="button"
+    data-bs-toggle="dropdown"
+    aria-expanded="false"
+  >
+    {{currentSource}}
+  </button>
+  <ul class="dropdown-menu">
+    <li>
+      <a class="dropdown-item" href="#" (click)="switchDataSource('Notes')">
+        Notes
+      </a>
+    </li>
+    <li>
+      <a class="dropdown-item" href="#" (click)="switchDataSource('Stats')">
+        Stats
+      </a>
+    </li>
+    <li>
+      <a
+        class="dropdown-item"
+        href="#"
+        (click)="switchDataSource('DriverRanking')"
+      >
+        Driver Ranking
+      </a>
+    </li>
+  </ul>
+</div>
+<h4>{{errorMessage}}</h4>
+<h4>{{progressMessage}}</h4>
+
+<ng-container [ngSwitch]="currentSource">
+  <!-- Notes Data Display. -->
+  <div *ngSwitchCase="'Notes'">
+    <table class="table">
+      <thead>
+        <tr>
+          <th scope="col" class="d-flex flex-row">
+            <div class="align-self-center">Team</div>
+            <div class="align-self-center" *ngIf="ascendingSort">
+              <i (click)="sortData()" class="bi bi-caret-up"></i>
+            </div>
+            <div class="align-self-center" *ngIf="!ascendingSort">
+              <i (click)="sortData()" class="bi bi-caret-down"></i>
+            </div>
+          </th>
+          <th scope="col">Match</th>
+          <th scope="col">Note</th>
+          <th scope="col">Keywords</th>
+          <th scope="col"></th>
+        </tr>
+      </thead>
+      <tbody>
+        <tr *ngFor="let note of noteList; index as i;">
+          <th scope="row">{{note.team()}}</th>
+          <!-- Placeholder for match number. -->
+          <td>0</td>
+          <td>{{note.notes()}}</td>
+          <td>{{parseKeywords(note)}}</td>
+          <!-- Delete Icon. -->
+          <td>
+            <button class="btn btn-danger" (click)="deleteData()">
+              <i class="bi bi-trash"></i>
+            </button>
+          </td>
+        </tr>
+      </tbody>
+    </table>
+  </div>
+  <!-- Stats Data Display. -->
+  <div *ngSwitchCase="'Stats'">
+    <table class="table">
+      <thead>
+        <tr>
+          <th scope="col" class="d-flex flex-row">
+            <div class="align-self-center">Match</div>
+            <div class="align-self-center" *ngIf="ascendingSort">
+              <i (click)="sortData()" class="bi bi-caret-up"></i>
+            </div>
+            <div class="align-self-center" *ngIf="!ascendingSort">
+              <i (click)="sortData()" class="bi bi-caret-down"></i>
+            </div>
+          </th>
+          <th scope="col">Team</th>
+          <th scope="col">Set</th>
+          <th scope="col">Comp Level</th>
+          <th scope="col"></th>
+        </tr>
+      </thead>
+      <tbody>
+        <tr *ngFor="let stat of statList; index as i;">
+          <th scope="row">{{stat.match()}}</th>
+          <td>{{stat.team()}}</td>
+          <td>{{stat.setNumber()}}</td>
+          <td>{{COMP_LEVEL_LABELS[stat.compLevel()]}}</td>
+          <!-- Delete Icon. -->
+          <td>
+            <button class="btn btn-danger" (click)="deleteData()">
+              <i class="bi bi-trash"></i>
+            </button>
+          </td>
+        </tr>
+      </tbody>
+    </table>
+  </div>
+  <!-- Driver Ranking Data Display. -->
+  <div *ngSwitchCase="'DriverRanking'">
+    <table class="table">
+      <thead>
+        <tr>
+          <th scope="col" class="d-flex flex-row">
+            <div class="align-self-center">Match</div>
+            <div class="align-self-center" *ngIf="ascendingSort">
+              <i (click)="sortData()" class="bi bi-caret-up"></i>
+            </div>
+            <div class="align-self-center" *ngIf="!ascendingSort">
+              <i (click)="sortData()" class="bi bi-caret-down"></i>
+            </div>
+          </th>
+          <th scope="col">Rank1</th>
+          <th scope="col">Rank2</th>
+          <th scope="col">Rank3</th>
+          <th scope="col"></th>
+        </tr>
+      </thead>
+      <tbody>
+        <tr *ngFor="let ranking of driverRankingList; index as i;">
+          <th scope="row">{{ranking.matchNumber()}}</th>
+          <td>{{ranking.rank1()}}</td>
+          <td>{{ranking.rank2()}}</td>
+          <td>{{ranking.rank3()}}</td>
+          <!-- Delete Icon. -->
+          <td>
+            <button class="btn btn-danger" (click)="deleteData()">
+              <i class="bi bi-trash"></i>
+            </button>
+          </td>
+        </tr>
+      </tbody>
+    </table>
+  </div>
+</ng-container>