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/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;
+  }
+}