Prevent scouts from scouting a non-existent match

We used to let people enter arbitrary match information, but the web
server rejects that information. It's better when we don't let people
enter arbitrary match information. This patch makes it so the "Next"
button is disabled on the "Team Selection" screen unless the match
information is correct.

Signed-off-by: Philipp Schrader <philipp.schrader@gmail.com>
Change-Id: I9203d04ab3c95ece4d84ede1042b2bac85e02936
diff --git a/scouting/www/entry/BUILD b/scouting/www/entry/BUILD
index 07c1d79..2884f23 100644
--- a/scouting/www/entry/BUILD
+++ b/scouting/www/entry/BUILD
@@ -11,7 +11,9 @@
     deps = [
         ":node_modules/@angular/forms",
         "//scouting/webserver/requests/messages:error_response_ts_fbs",
+        "//scouting/webserver/requests/messages:request_all_matches_response_ts_fbs",
         "//scouting/webserver/requests/messages:submit_actions_ts_fbs",
+        "//scouting/www/rpc",
         "@com_github_google_flatbuffers//ts:flatbuffers_ts",
     ],
 )
diff --git a/scouting/www/entry/entry.component.ts b/scouting/www/entry/entry.component.ts
index 8992ed8..56ee122 100644
--- a/scouting/www/entry/entry.component.ts
+++ b/scouting/www/entry/entry.component.ts
@@ -24,6 +24,8 @@
   ActionType,
   Action,
 } from '../../webserver/requests/messages/submit_actions_generated';
+import {Match} from '../../webserver/requests/messages/request_all_matches_response_generated';
+import {MatchListRequestor} from '@org_frc971/scouting/www/rpc';
 
 type Section =
   | 'Team Selection'
@@ -113,22 +115,80 @@
 
   section: Section = 'Team Selection';
   @Input() matchNumber: number = 1;
+  // TODO(phil): Change the type of teamNumber to a string.
   @Input() teamNumber: number = 1;
   @Input() setNumber: number = 1;
   @Input() compLevel: CompLevel = 'qm';
   @Input() skipTeamSelection = false;
 
+  matchList: Match[] = [];
+
   actionList: ActionT[] = [];
+  progressMessage: string = '';
   errorMessage: string = '';
   autoPhase: boolean = true;
   lastObject: ObjectType = null;
 
   matchStartTimestamp: number = 0;
 
+  teamSelectionIsValid = false;
+
+  constructor(private readonly matchListRequestor: MatchListRequestor) {}
+
   ngOnInit() {
     // When the user navigated from the match list, we can skip the team
     // selection. I.e. we trust that the user clicked the correct button.
     this.section = this.skipTeamSelection ? 'Init' : 'Team Selection';
+
+    if (this.section == 'Team Selection') {
+      this.fetchMatchList();
+    }
+  }
+
+  async fetchMatchList() {
+    this.progressMessage = 'Fetching match list. Please be patient.';
+    this.errorMessage = '';
+
+    try {
+      this.matchList = await this.matchListRequestor.fetchMatchList();
+      this.progressMessage = 'Successfully fetched match list.';
+    } catch (e) {
+      this.errorMessage = e;
+      this.progressMessage = '';
+    }
+  }
+
+  // This gets called when the user changes something on the Init screen.
+  // It makes sure that the user can't click "Next" until the information is
+  // valid.
+  updateTeamSelectionValidity(): void {
+    this.teamSelectionIsValid = this.matchIsInMatchList();
+  }
+
+  matchIsInMatchList(): boolean {
+    // If the user deletes the content of the teamNumber field, the value here
+    // is undefined. Guard against that.
+    if (this.teamNumber == null) {
+      return false;
+    }
+    const teamNumber = this.teamNumber.toString();
+
+    for (const match of this.matchList) {
+      if (
+        this.matchNumber == match.matchNumber() &&
+        this.setNumber == match.setNumber() &&
+        this.compLevel == match.compLevel() &&
+        (teamNumber === match.r1() ||
+          teamNumber === match.r2() ||
+          teamNumber === match.r3() ||
+          teamNumber === match.b1() ||
+          teamNumber === match.b2() ||
+          teamNumber === match.b3())
+      ) {
+        return true;
+      }
+    }
+    return false;
   }
 
   addAction(action: ActionT): void {
@@ -183,6 +243,10 @@
   }
 
   changeSectionTo(target: Section) {
+    // Clear the messages since they won't be relevant in the next section.
+    this.errorMessage = '';
+    this.progressMessage = '';
+
     this.section = target;
   }
 
diff --git a/scouting/www/entry/entry.ng.html b/scouting/www/entry/entry.ng.html
index 828e036..13cbe7f 100644
--- a/scouting/www/entry/entry.ng.html
+++ b/scouting/www/entry/entry.ng.html
@@ -14,6 +14,7 @@
       <label for="match_number">Match Number</label>
       <input
         [(ngModel)]="matchNumber"
+        (ngModelChange)="updateTeamSelectionValidity()"
         type="number"
         id="match_number"
         min="1"
@@ -24,6 +25,7 @@
       <label for="team_number">Team Number</label>
       <input
         [(ngModel)]="teamNumber"
+        (ngModelChange)="updateTeamSelectionValidity()"
         type="number"
         id="team_number"
         min="1"
@@ -34,6 +36,7 @@
       <label for="set_number">Set Number</label>
       <input
         [(ngModel)]="setNumber"
+        (ngModelChange)="updateTeamSelectionValidity()"
         type="number"
         id="set_number"
         min="1"
@@ -42,7 +45,12 @@
     </div>
     <div class="row">
       <label for="comp_level">Comp Level</label>
-      <select [(ngModel)]="compLevel" type="number" id="comp_level">
+      <select
+        [(ngModel)]="compLevel"
+        (ngModelChange)="updateTeamSelectionValidity()"
+        type="number"
+        id="comp_level"
+      >
         <option *ngFor="let level of COMP_LEVELS" [ngValue]="level">
           {{COMP_LEVEL_LABELS[level]}}
         </option>
@@ -51,7 +59,11 @@
     <div class="buttons">
       <!-- hack to right align the next button -->
       <div></div>
-      <button class="btn btn-primary" (click)="changeSectionTo('Init');">
+      <button
+        class="btn btn-primary"
+        (click)="changeSectionTo('Init');"
+        [disabled]="!teamSelectionIsValid"
+      >
         Next
       </button>
     </div>
@@ -313,5 +325,6 @@
     <h2>Successfully submitted data.</h2>
   </div>
 
+  <span class="progress_message" role="alert">{{ progressMessage }}</span>
   <span class="error_message" role="alert">{{ errorMessage }}</span>
 </ng-container>