Update Notes

Update Notes to include Match Number, Set Number, Comp Level, and No Show

Signed-off-by: Emily Markova <emily.markova@gmail.com>
Change-Id: I26550c0ea32b81bb8124d326cf1bef2bf912886a
diff --git a/scouting/db/db.go b/scouting/db/db.go
index 016ca95..b2feabe 100644
--- a/scouting/db/db.go
+++ b/scouting/db/db.go
@@ -110,6 +110,9 @@
 type NotesData struct {
 	ID             uint `gorm:"primaryKey"`
 	TeamNumber     string
+	MatchNumber    int32
+	SetNumber      int32
+	CompLevel      string
 	Notes          string
 	GoodDriving    bool
 	BadDriving     bool
@@ -118,6 +121,7 @@
 	GoodDefense    bool
 	BadDefense     bool
 	EasilyDefended bool
+	NoShow         bool
 }
 
 type Ranking struct {
@@ -444,6 +448,9 @@
 func (database *Database) AddNotes(data NotesData) error {
 	result := database.Create(&NotesData{
 		TeamNumber:     data.TeamNumber,
+		MatchNumber:    data.MatchNumber,
+		SetNumber:      data.SetNumber,
+		CompLevel:      data.CompLevel,
 		Notes:          data.Notes,
 		GoodDriving:    data.GoodDriving,
 		BadDriving:     data.BadDriving,
@@ -452,6 +459,7 @@
 		GoodDefense:    data.GoodDefense,
 		BadDefense:     data.BadDefense,
 		EasilyDefended: data.EasilyDefended,
+		NoShow:         data.NoShow,
 	})
 	return result.Error
 }
diff --git a/scouting/db/db_test.go b/scouting/db/db_test.go
index 882dfbc..c3b1143 100644
--- a/scouting/db/db_test.go
+++ b/scouting/db/db_test.go
@@ -1419,11 +1419,11 @@
 
 	expected := []string{"Note 1", "Note 3"}
 
-	err := fixture.db.AddNotes(NotesData{TeamNumber: "1234", Notes: "Note 1", GoodDriving: true, BadDriving: false, SolidPlacing: false, SketchyPlacing: true, GoodDefense: false, BadDefense: true, EasilyDefended: true})
+	err := fixture.db.AddNotes(NotesData{TeamNumber: "1234", MatchNumber: 5, SetNumber: 1, CompLevel: "quals", Notes: "Note 1", GoodDriving: true, BadDriving: false, SolidPlacing: false, SketchyPlacing: true, GoodDefense: false, BadDefense: true, EasilyDefended: true, NoShow: false})
 	check(t, err, "Failed to add Note")
-	err = fixture.db.AddNotes(NotesData{TeamNumber: "1235", Notes: "Note 2", GoodDriving: false, BadDriving: true, SolidPlacing: false, SketchyPlacing: true, GoodDefense: false, BadDefense: false, EasilyDefended: false})
+	err = fixture.db.AddNotes(NotesData{TeamNumber: "1235", MatchNumber: 54, SetNumber: 1, CompLevel: "quals", Notes: "Note 2", GoodDriving: false, BadDriving: true, SolidPlacing: false, SketchyPlacing: true, GoodDefense: false, BadDefense: false, EasilyDefended: false, NoShow: false})
 	check(t, err, "Failed to add Note")
-	err = fixture.db.AddNotes(NotesData{TeamNumber: "1234", Notes: "Note 3", GoodDriving: true, BadDriving: false, SolidPlacing: false, SketchyPlacing: true, GoodDefense: true, BadDefense: false, EasilyDefended: true})
+	err = fixture.db.AddNotes(NotesData{TeamNumber: "1234", MatchNumber: 23, SetNumber: 3, CompLevel: "quals", Notes: "Note 3", GoodDriving: true, BadDriving: false, SolidPlacing: false, SketchyPlacing: true, GoodDefense: true, BadDefense: false, EasilyDefended: true, NoShow: true})
 	check(t, err, "Failed to add Note")
 
 	actual, err := fixture.db.QueryNotes("1234")
diff --git a/scouting/scouting_test.cy.js b/scouting/scouting_test.cy.js
index 2de6879..7600bb5 100644
--- a/scouting/scouting_test.cy.js
+++ b/scouting/scouting_test.cy.js
@@ -309,6 +309,9 @@
     // Navigate to add team selection and add another team.
     clickButton('Add team');
     setInputTo('#team_number_notes', '1235');
+    setInputTo('#match_number_notes', 1);
+    setInputTo('#set_number_notes', 2);
+    setInputTo('#comp_level_notes', 'qm');
     clickButton('Select');
 
     // Add note and select keyword for second team.
@@ -328,16 +331,25 @@
 
     // Add first team.
     setInputTo('#team_number_notes', '1234');
+    setInputTo('#match_number_notes', 1);
+    setInputTo('#set_number_notes', 2);
+    setInputTo('#comp_level_notes', 'qm');
     clickButton('Select');
 
     // Add second team.
     clickButton('Add team');
     setInputTo('#team_number_notes', '1235');
+    setInputTo('#match_number_notes', 1);
+    setInputTo('#set_number_notes', 2);
+    setInputTo('#comp_level_notes', 'qm');
     clickButton('Select');
 
     // Add third team.
     clickButton('Add team');
     setInputTo('#team_number_notes', '1236');
+    setInputTo('#match_number_notes', 1);
+    setInputTo('#set_number_notes', 2);
+    setInputTo('#comp_level_notes', 'qm');
     clickButton('Select');
 
     for (let i = 1; i <= 3; i++) {
diff --git a/scouting/webserver/requests/debug/cli/cli_test.py b/scouting/webserver/requests/debug/cli/cli_test.py
index d11b506..cfdcd03 100644
--- a/scouting/webserver/requests/debug/cli/cli_test.py
+++ b/scouting/webserver/requests/debug/cli/cli_test.py
@@ -107,9 +107,10 @@
             "bad_driving": False,
             "solid_placing": False,
             "sketchy_placing": True,
-            "good_defense": False,
-            "bad_defense": False,
-            "easily_defended": False,
+            "no_show": False,
+            "match_number": 3,
+            "set_number": 1,
+            "comp_level": "qm",
         })
         exit_code, _, stderr = run_debug_cli(["-submitNotes", json_path])
         self.assertEqual(exit_code, 0, stderr)
@@ -132,7 +133,11 @@
             SketchyPlacing: (bool) true,
             GoodDefense: (bool) false,
             BadDefense: (bool) false,
-            EasilyDefended: (bool) false
+            EasilyDefended: (bool) false,
+            NoShow: (bool) false,
+            MatchNumber: (int32) 3,
+            SetNumber: (int32) 1,
+            CompLevel: (string) (len=2) "qm"
             }"""), stdout)
 
     def test_submit_and_request_driver_ranking(self):
diff --git a/scouting/webserver/requests/messages/request_all_notes_response.fbs b/scouting/webserver/requests/messages/request_all_notes_response.fbs
index 1afdf25..cd9b702 100644
--- a/scouting/webserver/requests/messages/request_all_notes_response.fbs
+++ b/scouting/webserver/requests/messages/request_all_notes_response.fbs
@@ -10,6 +10,10 @@
     good_defense:bool (id: 6);
     bad_defense:bool (id: 7);
     easily_defended:bool (id: 8);
+    no_show:bool (id: 9);
+    match_number:int (id: 10);
+    set_number:int (id: 11);
+    comp_level:string (id: 12);
 }
 
 table RequestAllNotesResponse {
diff --git a/scouting/webserver/requests/messages/submit_notes.fbs b/scouting/webserver/requests/messages/submit_notes.fbs
index 332b612..64226bc 100644
--- a/scouting/webserver/requests/messages/submit_notes.fbs
+++ b/scouting/webserver/requests/messages/submit_notes.fbs
@@ -10,6 +10,10 @@
     good_defense:bool (id: 6);
     bad_defense:bool (id: 7);
     easily_defended:bool (id: 8);
+    no_show:bool (id: 9);
+    match_number:int (id: 10);
+    set_number:int (id: 11);
+    comp_level:string (id: 12);
 }
 
 root_type SubmitNotes;
diff --git a/scouting/webserver/requests/requests.go b/scouting/webserver/requests/requests.go
index 7d6e98e..1536093 100644
--- a/scouting/webserver/requests/requests.go
+++ b/scouting/webserver/requests/requests.go
@@ -398,6 +398,10 @@
 		GoodDefense:    bool(request.GoodDefense()),
 		BadDefense:     bool(request.BadDefense()),
 		EasilyDefended: bool(request.EasilyDefended()),
+		NoShow:         bool(request.NoShow()),
+		MatchNumber:    request.MatchNumber(),
+		SetNumber:      request.SetNumber(),
+		CompLevel:      string(request.CompLevel()),
 	})
 	if err != nil {
 		respondWithError(w, http.StatusInternalServerError, fmt.Sprintf("Failed to insert notes: %v", err))
@@ -1060,6 +1064,10 @@
 			GoodDefense:    note.GoodDefense,
 			BadDefense:     note.BadDefense,
 			EasilyDefended: note.EasilyDefended,
+			NoShow:         note.NoShow,
+			MatchNumber:    note.MatchNumber,
+			CompLevel:      note.CompLevel,
+			SetNumber:      note.SetNumber,
 		})
 	}
 
diff --git a/scouting/webserver/requests/requests_test.go b/scouting/webserver/requests/requests_test.go
index 26fad0f..6968346 100644
--- a/scouting/webserver/requests/requests_test.go
+++ b/scouting/webserver/requests/requests_test.go
@@ -696,6 +696,10 @@
 		GoodDefense:    true,
 		BadDefense:     false,
 		EasilyDefended: true,
+		NoShow:         false,
+		MatchNumber:    4,
+		CompLevel:      "qm",
+		SetNumber:      1,
 	}).Pack(builder))
 
 	_, err := debug.SubmitNotes("http://localhost:8080", builder.FinishedBytes())
@@ -714,6 +718,10 @@
 			GoodDefense:    true,
 			BadDefense:     false,
 			EasilyDefended: true,
+			NoShow:         false,
+			MatchNumber:    4,
+			CompLevel:      "qm",
+			SetNumber:      1,
 		},
 	}
 
@@ -734,6 +742,10 @@
 			GoodDefense:    true,
 			BadDefense:     false,
 			EasilyDefended: true,
+			NoShow:         false,
+			MatchNumber:    4,
+			CompLevel:      "qm",
+			SetNumber:      1,
 		}},
 	}
 	scoutingServer := server.NewScoutingServer()
@@ -1098,6 +1110,10 @@
 				GoodDefense:    true,
 				BadDefense:     false,
 				EasilyDefended: false,
+				NoShow:         false,
+				MatchNumber:    4,
+				CompLevel:      "qm",
+				SetNumber:      1,
 			},
 			{
 				TeamNumber:     "972",
@@ -1109,6 +1125,10 @@
 				GoodDefense:    false,
 				BadDefense:     true,
 				EasilyDefended: false,
+				NoShow:         false,
+				MatchNumber:    1,
+				CompLevel:      "qm",
+				SetNumber:      2,
 			},
 		},
 	}
@@ -1137,6 +1157,10 @@
 				GoodDefense:    true,
 				BadDefense:     false,
 				EasilyDefended: false,
+				NoShow:         false,
+				MatchNumber:    4,
+				CompLevel:      "qm",
+				SetNumber:      1,
 			},
 			{
 				Team:           "972",
@@ -1148,6 +1172,10 @@
 				GoodDefense:    false,
 				BadDefense:     true,
 				EasilyDefended: false,
+				NoShow:         false,
+				MatchNumber:    1,
+				CompLevel:      "qm",
+				SetNumber:      2,
 			},
 		},
 	}
diff --git a/scouting/www/notes/notes.component.ts b/scouting/www/notes/notes.component.ts
index 6263d37..5afc8e2 100644
--- a/scouting/www/notes/notes.component.ts
+++ b/scouting/www/notes/notes.component.ts
@@ -33,6 +33,18 @@
 // for all the teams being scouted.
 type Section = 'TeamSelection' | 'Data';
 
+const COMP_LEVELS = ['qm', 'ef', 'qf', 'sf', 'f'] as const;
+type CompLevel = typeof COMP_LEVELS[number];
+
+// TODO(phil): Deduplicate with match_list.component.ts.
+const COMP_LEVEL_LABELS: Record<CompLevel, string> = {
+  qm: 'Qualifications',
+  ef: 'Eighth Finals',
+  qf: 'Quarter Finals',
+  sf: 'Semi Finals',
+  f: 'Finals',
+};
+
 // 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
@@ -45,12 +57,16 @@
   goodDefense: boolean;
   badDefense: boolean;
   easilyDefended: boolean;
+  noShow: boolean;
 }
 
 interface Input {
   teamNumber: string;
   notesData: string;
   keywordsData: Keywords;
+  matchNumber: number;
+  setNumber: number;
+  compLevel: string;
 }
 
 const KEYWORD_CHECKBOX_LABELS = {
@@ -61,6 +77,7 @@
   goodDefense: 'Good Defense',
   badDefense: 'Bad Defense',
   easilyDefended: 'Easily Defended',
+  noShow: 'No Show',
 } as const;
 
 @Component({
@@ -71,6 +88,8 @@
 export class Notes {
   // Re-export KEYWORD_CHECKBOX_LABELS so that we can
   // use it in the checkbox properties.
+  readonly COMP_LEVELS = COMP_LEVELS;
+  readonly COMP_LEVEL_LABELS = COMP_LEVEL_LABELS;
   readonly KEYWORD_CHECKBOX_LABELS = KEYWORD_CHECKBOX_LABELS;
 
   // Necessary in order to iterate the keys of KEYWORD_CHECKBOX_LABELS.
@@ -79,7 +98,10 @@
   section: Section = 'TeamSelection';
 
   errorMessage = '';
-  teamNumberSelection: string = '971';
+  teamNumber: string = '1';
+  matchNumber: number = 1;
+  setNumber: number = 1;
+  compLevel: CompLevel = 'qm';
 
   // Data inputted by user is stored in this array.
   // Includes the team number, notes, and keyword selection.
@@ -108,10 +130,17 @@
     }
   }
 
-  setTeamNumber() {
+  setTeamData() {
     let data: Input = {
-      teamNumber: this.teamNumberSelection,
-      notesData: 'Match: \nAuto: \nTeleop: \nEndgame: ',
+      teamNumber: this.teamNumber,
+      notesData:
+        'Match ' +
+        this.matchNumber +
+        ' Set ' +
+        this.setNumber +
+        ' ' +
+        COMP_LEVEL_LABELS[this.compLevel] +
+        ' \nAuto: \nTeleop: \nEndgame: ',
       keywordsData: {
         goodDriving: false,
         badDriving: false,
@@ -120,7 +149,11 @@
         goodDefense: false,
         badDefense: false,
         easilyDefended: false,
+        noShow: false,
       },
+      matchNumber: this.matchNumber,
+      setNumber: this.setNumber,
+      compLevel: this.compLevel,
     };
 
     this.newData.push(data);
@@ -146,6 +179,7 @@
       const dataFb = builder.createString(this.newData[i].notesData);
 
       const teamNumber = builder.createString(this.newData[i].teamNumber);
+      const compLevel = builder.createString(this.newData[i].compLevel);
 
       builder.finish(
         SubmitNotes.createSubmitNotes(
@@ -158,7 +192,11 @@
           this.newData[i].keywordsData.sketchyPlacing,
           this.newData[i].keywordsData.goodDefense,
           this.newData[i].keywordsData.badDefense,
-          this.newData[i].keywordsData.easilyDefended
+          this.newData[i].keywordsData.easilyDefended,
+          this.newData[i].keywordsData.noShow,
+          this.newData[i].matchNumber,
+          this.newData[i].setNumber,
+          compLevel
         )
       );
 
diff --git a/scouting/www/notes/notes.ng.html b/scouting/www/notes/notes.ng.html
index d7262b3..7c5e48b 100644
--- a/scouting/www/notes/notes.ng.html
+++ b/scouting/www/notes/notes.ng.html
@@ -4,15 +4,41 @@
 
 <ng-container [ngSwitch]="section">
   <div *ngSwitchCase="'TeamSelection'">
-    <label id="team_number_label" class="label" for="team_number_notes">
-      Team Number
-    </label>
-    <input
-      [(ngModel)]="teamNumberSelection"
-      type="text"
-      id="team_number_notes"
-    />
-    <button class="btn btn-primary" (click)="setTeamNumber()">Select</button>
+    <div class="row">
+      <label id="team_number_label" class="label" for="team_number_notes">
+        Team Number
+      </label>
+      <input [(ngModel)]="teamNumber" type="text" id="team_number_notes" />
+    </div>
+    <div class="row">
+      <label for="match_number_notes">Match Number</label>
+      <input
+        [(ngModel)]="matchNumber"
+        type="number"
+        id="match_number_notes"
+        min="1"
+        max="999"
+      />
+    </div>
+    <div class="row">
+      <label for="set_number_notes">Set Number</label>
+      <input
+        [(ngModel)]="setNumber"
+        type="number"
+        id="set_number_notes"
+        min="1"
+        max="10"
+      />
+    </div>
+    <div class="row">
+      <label for="comp_level_notes">Comp Level</label>
+      <select [(ngModel)]="compLevel" type="number" id="comp_level_notes">
+        <option *ngFor="let level of COMP_LEVELS" [ngValue]="level">
+          {{COMP_LEVEL_LABELS[level]}}
+        </option>
+      </select>
+    </div>
+    <button class="btn btn-primary" (click)="setTeamData()">Select</button>
   </div>
 
   <div *ngSwitchCase="'Data'">
@@ -74,7 +100,7 @@
         <!--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"
+            *ngFor="let key of Object.keys(KEYWORD_CHECKBOX_LABELS) | slice:4:(Object.keys(KEYWORD_CHECKBOX_LABELS).length); let k = index"
           >
             <div class="form-check">
               <input
diff --git a/scouting/www/view/view.component.ts b/scouting/www/view/view.component.ts
index 8aa3784..d88c25a 100644
--- a/scouting/www/view/view.component.ts
+++ b/scouting/www/view/view.component.ts
@@ -80,9 +80,7 @@
     if (!this.ascendingSort) {
       this.driverRankingList.sort((a, b) => b.matchNumber() - a.matchNumber());
       this.noteList.sort(function (a, b) {
-        return b[0]
-          .team()
-          .localeCompare(a[0].team(), undefined, {numeric: true});
+        return b.team().localeCompare(a.team(), undefined, {numeric: true});
       });
       this.pitImageList.sort(function (a, b) {
         return b[0]
@@ -93,9 +91,7 @@
     } else {
       this.driverRankingList.sort((a, b) => a.matchNumber() - b.matchNumber());
       this.noteList.sort(function (a, b) {
-        return b[0]
-          .team()
-          .localeCompare(a[0].team(), undefined, {numeric: true});
+        return a.team().localeCompare(b.team(), undefined, {numeric: true});
       });
       this.pitImageList.sort(function (a, b) {
         return a[0]
@@ -311,6 +307,9 @@
     if (entry.badDriving()) {
       parsedKeywords += 'Bad Driving ';
     }
+    if (entry.noShow()) {
+      parsedKeywords += 'No Show ';
+    }
     if (entry.solidPlacing()) {
       parsedKeywords += 'Solid Placing ';
     }
diff --git a/scouting/www/view/view.ng.html b/scouting/www/view/view.ng.html
index 14e8c8a..f870178 100644
--- a/scouting/www/view/view.ng.html
+++ b/scouting/www/view/view.ng.html
@@ -71,7 +71,7 @@
             </div>
           </th>
           <th scope="col">Match</th>
-          <th scope="col">Note</th>
+          <th scope="col">Notes</th>
           <th scope="col">Keywords</th>
           <th scope="col"></th>
         </tr>
@@ -79,8 +79,7 @@
       <tbody>
         <tr *ngFor="let note of noteList; index as i;">
           <th scope="row">{{note.team()}}</th>
-          <!-- Placeholder for match number. -->
-          <td>0</td>
+          <th scope="row">{{note.matchNumber()}}</th>
           <td>{{note.notes()}}</td>
           <td>{{parseKeywords(note)}}</td>
           <!-- Delete Icon. -->