[Scouting App] Note scout multiple robots and add keywords

Note scouts often scout multiple robots so the scouting app should support it and
checkboxes were added to select keywords/tags. These tags will likely have to be modified
according to 2023's game.

Mobile UI Preview: https://ibb.co/QdFxwGj

Change-Id: If4fcb3ee97da5f52e428cb0a4b0a8401b4700a02
Signed-off-by: Filip Kujawa <filip.j.kujawa@gmail.com>
diff --git a/scouting/www/notes/notes.component.css b/scouting/www/notes/notes.component.css
index 869bdab..67b2351 100644
--- a/scouting/www/notes/notes.component.css
+++ b/scouting/www/notes/notes.component.css
@@ -6,7 +6,6 @@
   width: calc(100% - 20px);
 }
 
-.buttons {
-  display: flex;
-  justify-content: space-between;
+.container-main {
+  padding-left: 20px;
 }
diff --git a/scouting/www/notes/notes.component.ts b/scouting/www/notes/notes.component.ts
index 0f0eb82..86df425 100644
--- a/scouting/www/notes/notes.component.ts
+++ b/scouting/www/notes/notes.component.ts
@@ -9,88 +9,146 @@
 import {SubmitNotes} from 'org_frc971/scouting/webserver/requests/messages/submit_notes_generated';
 import {SubmitNotesResponse} from 'org_frc971/scouting/webserver/requests/messages/submit_notes_response_generated';
 
+/*
+For new games, the keywords being used will likely need to be updated.
+To update the keywords complete the following: 
+  1) Update the Keywords Interface and KEYWORD_CHECKBOX_LABELS in notes.component.ts
+    The keys of Keywords and KEYWORD_CHECKBOX_LABELS should match.
+  2) In notes.component.ts, update the setTeamNumber() method with the new keywords.
+  3) Add/Edit the new keywords in /scouting/webserver/requests/messages/submit_notes.fbs.
+  4) In notes.component.ts, update the submitData() method with the newKeywords 
+    so that it matches the updated flatbuffer
+  5) In db.go, update the NotesData struct and the 
+    AddNotes method with the new keywords        
+  6) In db_test.go update the TestNotes method so the test uses the keywords
+  7) Update the submitNoteScoutingHandler in requests.go with the new keywords
+  8) Finally, update the corresponding test in requests_test.go (TestSubmitNotes)
+  
+  Note: If you change the number of keywords you might need to 
+    update how they are displayed in notes.ng.html 
+*/
+
+// TeamSelection: Display form to add a team to the teams being scouted.
+// Data: Display the note textbox and keyword selection form
+// for all the teams being scouted.
 type Section = 'TeamSelection' | 'Data';
 
-interface Note {
-  readonly data: string;
+// Every keyword checkbox corresponds to a boolean.
+// If the boolean is True, the checkbox is selected
+// and the note scout saw that the robot being scouted
+// displayed said property (ex. Driving really well -> goodDriving)
+interface Keywords {
+  goodDriving: boolean;
+  badDriving: boolean;
+  sketchyClimb: boolean;
+  solidClimb: boolean;
+  goodDefense: boolean;
+  badDefense: boolean;
 }
 
+interface Input {
+  teamNumber: number;
+  notesData: string;
+  keywordsData: Keywords;
+}
+
+const KEYWORD_CHECKBOX_LABELS = {
+  goodDriving: 'Good Driving',
+  badDriving: 'Bad Driving',
+  solidClimb: 'Solid Climb',
+  sketchyClimb: 'Sketchy Climb',
+  goodDefense: 'Good Defense',
+  badDefense: 'Bad Defense',
+} as const;
+
 @Component({
   selector: 'frc971-notes',
   templateUrl: './notes.ng.html',
   styleUrls: ['../common.css', './notes.component.css'],
 })
 export class Notes {
+  // Re-export KEYWORD_CHECKBOX_LABELS so that we can
+  // use it in the checkbox properties.
+  readonly KEYWORD_CHECKBOX_LABELS = KEYWORD_CHECKBOX_LABELS;
+
+  // Necessary in order to iterate the keys of KEYWORD_CHECKBOX_LABELS.
+  Object = Object;
+
   section: Section = 'TeamSelection';
-  notes: Note[] = [];
 
   errorMessage = '';
+  teamNumberSelection: number = 971;
 
-  teamNumber: number = 971;
-  newData = '';
+  // Data inputted by user is stored in this array.
+  // Includes the team number, notes, and keyword selection.
+  newData: Input[] = [];
 
-  async setTeamNumber() {
-    const builder = new Builder();
-    RequestNotesForTeam.startRequestNotesForTeam(builder);
-    RequestNotesForTeam.addTeam(builder, this.teamNumber);
-    builder.finish(RequestNotesForTeam.endRequestNotesForTeam(builder));
+  setTeamNumber() {
+    let data: Input = {
+      teamNumber: this.teamNumberSelection,
+      notesData: '',
+      keywordsData: {
+        goodDriving: false,
+        badDriving: false,
+        solidClimb: false,
+        sketchyClimb: false,
+        goodDefense: false,
+        badDefense: false,
+      },
+    };
 
-    const buffer = builder.asUint8Array();
-    const res = await fetch('/requests/request/notes_for_team', {
-      method: 'POST',
-      body: buffer,
-    });
+    this.newData.push(data);
+    this.section = 'Data';
+  }
 
-    const resBuffer = await res.arrayBuffer();
-    const fbBuffer = new ByteBuffer(new Uint8Array(resBuffer));
-
-    if (res.ok) {
-      this.notes = [];
-      const parsedResponse =
-        RequestNotesForTeamResponse.getRootAsRequestNotesForTeamResponse(
-          fbBuffer
-        );
-      for (let i = 0; i < parsedResponse.notesLength(); i++) {
-        const fbNote = parsedResponse.notes(i);
-        this.notes.push({data: fbNote.data()});
-      }
-      this.section = 'Data';
+  removeTeam(index: number) {
+    this.newData.splice(index, 1);
+    if (this.newData.length == 0) {
+      this.section = 'TeamSelection';
     } else {
-      const parsedResponse = ErrorResponse.getRootAsErrorResponse(fbBuffer);
-
-      const errorMessage = parsedResponse.errorMessage();
-      this.errorMessage = `Received ${res.status} ${res.statusText}: "${errorMessage}"`;
+      this.section = 'Data';
     }
   }
 
-  changeTeam() {
+  addTeam() {
     this.section = 'TeamSelection';
   }
 
   async submitData() {
-    const builder = new Builder();
-    const dataFb = builder.createString(this.newData);
-    builder.finish(
-      SubmitNotes.createSubmitNotes(builder, this.teamNumber, dataFb)
-    );
+    for (let i = 0; i < this.newData.length; i++) {
+      const builder = new Builder();
+      const dataFb = builder.createString(this.newData[i].notesData);
+      builder.finish(
+        SubmitNotes.createSubmitNotes(
+          builder,
+          this.newData[i].teamNumber,
+          dataFb,
+          this.newData[i].keywordsData.goodDriving,
+          this.newData[i].keywordsData.badDriving,
+          this.newData[i].keywordsData.sketchyClimb,
+          this.newData[i].keywordsData.solidClimb,
+          this.newData[i].keywordsData.goodDefense,
+          this.newData[i].keywordsData.badDefense
+        )
+      );
 
-    const buffer = builder.asUint8Array();
-    const res = await fetch('/requests/submit/submit_notes', {
-      method: 'POST',
-      body: buffer,
-    });
+      const buffer = builder.asUint8Array();
+      const res = await fetch('/requests/submit/submit_notes', {
+        method: 'POST',
+        body: buffer,
+      });
 
-    if (res.ok) {
-      this.newData = '';
-      this.errorMessage = '';
-      await this.setTeamNumber();
-    } else {
-      const resBuffer = await res.arrayBuffer();
-      const fbBuffer = new ByteBuffer(new Uint8Array(resBuffer));
-      const parsedResponse = ErrorResponse.getRootAsErrorResponse(fbBuffer);
-
-      const errorMessage = parsedResponse.errorMessage();
-      this.errorMessage = `Received ${res.status} ${res.statusText}: "${errorMessage}"`;
+      if (!res.ok) {
+        const resBuffer = await res.arrayBuffer();
+        const fbBuffer = new ByteBuffer(new Uint8Array(resBuffer));
+        const parsedResponse = ErrorResponse.getRootAsErrorResponse(fbBuffer);
+        const errorMessage = parsedResponse.errorMessage();
+        this.errorMessage = `Received ${res.status} ${res.statusText}: "${errorMessage}"`;
+      }
     }
+
+    this.newData = [];
+    this.errorMessage = '';
+    this.section = 'TeamSelection';
   }
 }
diff --git a/scouting/www/notes/notes.ng.html b/scouting/www/notes/notes.ng.html
index a69ba88..af48fd9 100644
--- a/scouting/www/notes/notes.ng.html
+++ b/scouting/www/notes/notes.ng.html
@@ -2,9 +2,9 @@
 
 <ng-container [ngSwitch]="section">
   <div *ngSwitchCase="'TeamSelection'">
-    <label for="team_number_notes">Team Number</label>
+    <label class="label" for="team_number_notes">Team Number</label>
     <input
-      [(ngModel)]="teamNumber"
+      [(ngModel)]="teamNumberSelection"
       type="number"
       id="team_number_notes"
       min="1"
@@ -14,17 +14,85 @@
   </div>
 
   <div *ngSwitchCase="'Data'">
-    <h3>Scouting team: {{teamNumber}}</h3>
-    <ul *ngFor="let note of notes">
-      <li class="note">{{ note.data }}</li>
-    </ul>
-    <textarea class="text-input" [(ngModel)]="newData"></textarea>
-    <div class="buttons">
-      <button class="btn btn-primary" (click)="changeTeam()">
-        Change team
-      </button>
-      <button class="btn btn-primary" (click)="submitData()">Submit</button>
+    <div class="container-main" *ngFor="let team of newData; let i = index">
+      <div class="pt-2 pb-2">
+        <div class="d-flex flex-row">
+          <div>
+            <button
+              class="btn bg-transparent ml-10 md-5"
+              (click)="removeTeam(i)"
+            >
+              &#10006;
+              <!--X Symbol-->
+            </button>
+          </div>
+          <div><h3>{{team.teamNumber}}</h3></div>
+        </div>
+        <div class="">
+          <textarea
+            class="text-input"
+            [(ngModel)]="newData[i].notesData"
+          ></textarea>
+        </div>
+        <!--Key Word Checkboxes-->
+        <!--Row 1 (Prevent Overflow on mobile by splitting checkboxes into 2 rows)-->
+        <!--Slice KEYWORD_CHECKBOX_LABELS using https://angular.io/api/common/SlicePipe-->
+        <div class="d-flex flex-row justify-content-around">
+          <div
+            *ngFor="let key of Object.keys(KEYWORD_CHECKBOX_LABELS) | slice:0:((Object.keys(KEYWORD_CHECKBOX_LABELS).length)/2); let k = index"
+          >
+            <div class="form-check">
+              <input
+                class="form-check-input"
+                [(ngModel)]="newData[i]['keywordsData'][key]"
+                type="checkbox"
+                id="{{KEYWORD_CHECKBOX_LABELS[key]}}_{{i}}"
+                name="{{KEYWORD_CHECKBOX_LABELS[key]}}"
+              />
+              <label
+                class="form-check-label"
+                for="{{KEYWORD_CHECKBOX_LABELS[key]}}_{{i}}"
+              >
+                {{KEYWORD_CHECKBOX_LABELS[key]}}
+              </label>
+              <br />
+            </div>
+          </div>
+        </div>
+        <!--Row 2 (Prevent Overflow on mobile by splitting checkboxes into 2 rows)-->
+        <div class="d-flex flex-row justify-content-around">
+          <div
+            *ngFor="let key of Object.keys(KEYWORD_CHECKBOX_LABELS) | slice:3:(Object.keys(KEYWORD_CHECKBOX_LABELS).length); let k = index"
+          >
+            <div class="form-check">
+              <input
+                class="form-check-input"
+                [(ngModel)]="newData[i]['keywordsData'][key]"
+                type="checkbox"
+                id="{{KEYWORD_CHECKBOX_LABELS[key]}}"
+                name="{{KEYWORD_CHECKBOX_LABELS[key]}}"
+              />
+              <label
+                class="form-check-label"
+                for="{{KEYWORD_CHECKBOX_LABELS[key]}}"
+              >
+                {{KEYWORD_CHECKBOX_LABELS[key]}}
+              </label>
+              <br />
+            </div>
+          </div>
+        </div>
+      </div>
+    </div>
+    <div class="d-flex flex-row justify-content-center pt-2">
+      <div>
+        <button class="btn btn-secondary" (click)="addTeam()">Add team</button>
+      </div>
+      <div>
+        <button class="btn btn-success" (click)="submitData()">Submit</button>
+      </div>
     </div>
   </div>
+
   <div class="error">{{errorMessage}}</div>
 </ng-container>