[Scouting App] Add Driver Ranking

Move the driver ranking data collection from google forms to the scouting app.
The data collected is used to calculate a overall driver rank for the picklist.

Signed-off-by: Filip Kujawa <filip.j.kujawa@gmail.com>
Change-Id: Id459e8dec1fa79c1a9f49cc40ffa83014e51db16
diff --git a/BUILD b/BUILD
index 9368a1f..4a4baa5 100644
--- a/BUILD
+++ b/BUILD
@@ -32,6 +32,8 @@
 # gazelle:resolve go github.com/frc971/971-Robot-Code/scouting/webserver/requests/messages/request_shift_schedule_response //scouting/webserver/requests/messages:request_shift_schedule_response_go_fbs
 # gazelle:resolve go github.com/frc971/971-Robot-Code/scouting/webserver/requests/messages/submit_shift_schedule //scouting/webserver/requests/messages:submit_shift_schedule_go_fbs
 # gazelle:resolve go github.com/frc971/971-Robot-Code/scouting/webserver/requests/messages/submit_shift_schedule_response //scouting/webserver/requests/messages:submit_shift_schedule_response_go_fbs
+# gazelle:resolve go github.com/frc971/971-Robot-Code/scouting/webserver/requests/messages/submit_driver_ranking //scouting/webserver/requests/messages:submit_driver_ranking_go_fbs
+# gazelle:resolve go github.com/frc971/971-Robot-Code/scouting/webserver/requests/messages/submit_driver_ranking_response //scouting/webserver/requests/messages:submit_driver_ranking_response_go_fbs
 
 gazelle(
     name = "gazelle",
diff --git a/scouting/db/db.go b/scouting/db/db.go
index 82f7fa8..55f2310 100644
--- a/scouting/db/db.go
+++ b/scouting/db/db.go
@@ -84,6 +84,20 @@
 	Rank, Dq           int32
 }
 
+type DriverRankingData struct {
+	// Each entry in the table is a single scout's ranking.
+	// Multiple scouts can submit a driver ranking for the same
+	// teams in the same match.
+	// The teams being ranked are stored in Rank1, Rank2, Rank3,
+	// Rank1 being the best driving and Rank3 being the worst driving.
+
+	ID          uint `gorm:"primaryKey"`
+	MatchNumber int32
+	Rank1       int32
+	Rank2       int32
+	Rank3       int32
+}
+
 // Opens a database at the specified port on localhost. We currently don't
 // support connecting to databases on other hosts.
 func NewDatabase(user string, password string, port int) (*Database, error) {
@@ -99,7 +113,7 @@
 		return nil, errors.New(fmt.Sprint("Failed to connect to postgres: ", err))
 	}
 
-	err = database.AutoMigrate(&Match{}, &Shift{}, &Stats{}, &NotesData{}, &Ranking{})
+	err = database.AutoMigrate(&Match{}, &Shift{}, &Stats{}, &NotesData{}, &Ranking{}, &DriverRankingData{})
 	if err != nil {
 		database.Delete()
 		return nil, errors.New(fmt.Sprint("Failed to create/migrate tables: ", err))
@@ -272,3 +286,19 @@
 	})
 	return result.Error
 }
+
+func (database *Database) AddDriverRanking(data DriverRankingData) error {
+	result := database.Create(&DriverRankingData{
+		MatchNumber: data.MatchNumber,
+		Rank1:       data.Rank1,
+		Rank2:       data.Rank2,
+		Rank3:       data.Rank3,
+	})
+	return result.Error
+}
+
+func (database *Database) QueryDriverRanking(MatchNumber int) ([]DriverRankingData, error) {
+	var data []DriverRankingData
+	result := database.Where("match_number = ?", MatchNumber).Find(&data)
+	return data, result.Error
+}
diff --git a/scouting/db/db_test.go b/scouting/db/db_test.go
index 1e11008..460b177 100644
--- a/scouting/db/db_test.go
+++ b/scouting/db/db_test.go
@@ -729,3 +729,33 @@
 		t.Errorf("Got %#v,\nbut expected %#v.", actual, expected)
 	}
 }
+
+func TestDriverRanking(t *testing.T) {
+	fixture := createDatabase(t)
+	defer fixture.TearDown()
+
+	expected := []DriverRankingData{
+		{ID: 1, MatchNumber: 12, Rank1: 1234, Rank2: 1235, Rank3: 1236},
+		{ID: 2, MatchNumber: 12, Rank1: 1236, Rank2: 1235, Rank3: 1234},
+	}
+
+	err := fixture.db.AddDriverRanking(
+		DriverRankingData{MatchNumber: 12, Rank1: 1234, Rank2: 1235, Rank3: 1236},
+	)
+	check(t, err, "Failed to add Driver Ranking")
+	err = fixture.db.AddDriverRanking(
+		DriverRankingData{MatchNumber: 12, Rank1: 1236, Rank2: 1235, Rank3: 1234},
+	)
+	check(t, err, "Failed to add Driver Ranking")
+	err = fixture.db.AddDriverRanking(
+		DriverRankingData{MatchNumber: 13, Rank1: 1235, Rank2: 1234, Rank3: 1236},
+	)
+	check(t, err, "Failed to add Driver Ranking")
+
+	actual, err := fixture.db.QueryDriverRanking(12)
+	check(t, err, "Failed to get Driver Ranking")
+
+	if !reflect.DeepEqual(expected, actual) {
+		t.Errorf("Got %#v,\nbut expected %#v.", actual, expected)
+	}
+}
diff --git a/scouting/webserver/requests/BUILD b/scouting/webserver/requests/BUILD
index 87575a9..5c9ede4 100644
--- a/scouting/webserver/requests/BUILD
+++ b/scouting/webserver/requests/BUILD
@@ -24,6 +24,8 @@
         "//scouting/webserver/requests/messages:request_shift_schedule_response_go_fbs",
         "//scouting/webserver/requests/messages:submit_data_scouting_go_fbs",
         "//scouting/webserver/requests/messages:submit_data_scouting_response_go_fbs",
+        "//scouting/webserver/requests/messages:submit_driver_ranking_go_fbs",
+        "//scouting/webserver/requests/messages:submit_driver_ranking_response_go_fbs",
         "//scouting/webserver/requests/messages:submit_notes_go_fbs",
         "//scouting/webserver/requests/messages:submit_notes_response_go_fbs",
         "//scouting/webserver/requests/messages:submit_shift_schedule_go_fbs",
@@ -56,6 +58,7 @@
         "//scouting/webserver/requests/messages:request_shift_schedule_response_go_fbs",
         "//scouting/webserver/requests/messages:submit_data_scouting_go_fbs",
         "//scouting/webserver/requests/messages:submit_data_scouting_response_go_fbs",
+        "//scouting/webserver/requests/messages:submit_driver_ranking_go_fbs",
         "//scouting/webserver/requests/messages:submit_notes_go_fbs",
         "//scouting/webserver/requests/messages:submit_shift_schedule_go_fbs",
         "//scouting/webserver/server",
diff --git a/scouting/webserver/requests/debug/BUILD b/scouting/webserver/requests/debug/BUILD
index 04c4ffa..f826831 100644
--- a/scouting/webserver/requests/debug/BUILD
+++ b/scouting/webserver/requests/debug/BUILD
@@ -15,6 +15,7 @@
         "//scouting/webserver/requests/messages:request_notes_for_team_response_go_fbs",
         "//scouting/webserver/requests/messages:request_shift_schedule_response_go_fbs",
         "//scouting/webserver/requests/messages:submit_data_scouting_response_go_fbs",
+        "//scouting/webserver/requests/messages:submit_driver_ranking_response_go_fbs",
         "//scouting/webserver/requests/messages:submit_notes_response_go_fbs",
         "//scouting/webserver/requests/messages:submit_shift_schedule_response_go_fbs",
         "@com_github_google_flatbuffers//go:go_default_library",
diff --git a/scouting/webserver/requests/debug/debug.go b/scouting/webserver/requests/debug/debug.go
index b3df518..fc0896c 100644
--- a/scouting/webserver/requests/debug/debug.go
+++ b/scouting/webserver/requests/debug/debug.go
@@ -17,6 +17,7 @@
 	"github.com/frc971/971-Robot-Code/scouting/webserver/requests/messages/request_notes_for_team_response"
 	"github.com/frc971/971-Robot-Code/scouting/webserver/requests/messages/request_shift_schedule_response"
 	"github.com/frc971/971-Robot-Code/scouting/webserver/requests/messages/submit_data_scouting_response"
+	"github.com/frc971/971-Robot-Code/scouting/webserver/requests/messages/submit_driver_ranking_response"
 	"github.com/frc971/971-Robot-Code/scouting/webserver/requests/messages/submit_notes_response"
 	"github.com/frc971/971-Robot-Code/scouting/webserver/requests/messages/submit_shift_schedule_response"
 	flatbuffers "github.com/google/flatbuffers/go"
@@ -157,3 +158,9 @@
 		server+"/requests/submit/shift_schedule", requestBytes,
 		submit_shift_schedule_response.GetRootAsSubmitShiftScheduleResponse)
 }
+
+func SubmitDriverRanking(server string, requestBytes []byte) (*submit_driver_ranking_response.SubmitDriverRankingResponseT, error) {
+	return sendMessage[submit_driver_ranking_response.SubmitDriverRankingResponseT](
+		server+"/requests/submit/submit_driver_ranking", requestBytes,
+		submit_driver_ranking_response.GetRootAsSubmitDriverRankingResponse)
+}
diff --git a/scouting/webserver/requests/messages/BUILD b/scouting/webserver/requests/messages/BUILD
index b2d21a2..c14a857 100644
--- a/scouting/webserver/requests/messages/BUILD
+++ b/scouting/webserver/requests/messages/BUILD
@@ -21,6 +21,8 @@
     "request_shift_schedule_response",
     "submit_shift_schedule",
     "submit_shift_schedule_response",
+    "submit_driver_ranking",
+    "submit_driver_ranking_response",
 )
 
 filegroup(
diff --git a/scouting/webserver/requests/messages/submit_driver_ranking.fbs b/scouting/webserver/requests/messages/submit_driver_ranking.fbs
new file mode 100644
index 0000000..ac1e218
--- /dev/null
+++ b/scouting/webserver/requests/messages/submit_driver_ranking.fbs
@@ -0,0 +1,10 @@
+namespace scouting.webserver.requests;
+
+table SubmitDriverRanking {
+    matchNumber:int (id: 0);
+    rank1:int (id: 1);
+    rank2:int (id: 2);
+    rank3:int (id: 3);
+}
+
+root_type SubmitDriverRanking;
diff --git a/scouting/webserver/requests/messages/submit_driver_ranking_response.fbs b/scouting/webserver/requests/messages/submit_driver_ranking_response.fbs
new file mode 100644
index 0000000..78c6445
--- /dev/null
+++ b/scouting/webserver/requests/messages/submit_driver_ranking_response.fbs
@@ -0,0 +1,8 @@
+namespace scouting.webserver.requests;
+
+table SubmitDriverRankingResponse {
+    // empty response
+}
+
+root_type SubmitDriverRankingResponse;
+
diff --git a/scouting/webserver/requests/requests.go b/scouting/webserver/requests/requests.go
index e33e82d..12bc3da 100644
--- a/scouting/webserver/requests/requests.go
+++ b/scouting/webserver/requests/requests.go
@@ -27,6 +27,8 @@
 	"github.com/frc971/971-Robot-Code/scouting/webserver/requests/messages/request_shift_schedule_response"
 	"github.com/frc971/971-Robot-Code/scouting/webserver/requests/messages/submit_data_scouting"
 	"github.com/frc971/971-Robot-Code/scouting/webserver/requests/messages/submit_data_scouting_response"
+	"github.com/frc971/971-Robot-Code/scouting/webserver/requests/messages/submit_driver_ranking"
+	"github.com/frc971/971-Robot-Code/scouting/webserver/requests/messages/submit_driver_ranking_response"
 	"github.com/frc971/971-Robot-Code/scouting/webserver/requests/messages/submit_notes"
 	"github.com/frc971/971-Robot-Code/scouting/webserver/requests/messages/submit_notes_response"
 	"github.com/frc971/971-Robot-Code/scouting/webserver/requests/messages/submit_shift_schedule"
@@ -53,6 +55,8 @@
 type RequestShiftScheduleResponseT = request_shift_schedule_response.RequestShiftScheduleResponseT
 type SubmitShiftSchedule = submit_shift_schedule.SubmitShiftSchedule
 type SubmitShiftScheduleResponseT = submit_shift_schedule_response.SubmitShiftScheduleResponseT
+type SubmitDriverRanking = submit_driver_ranking.SubmitDriverRanking
+type SubmitDriverRankingResponseT = submit_driver_ranking_response.SubmitDriverRankingResponseT
 
 // The interface we expect the database abstraction to conform to.
 // We use an interface here because it makes unit testing easier.
@@ -68,6 +72,7 @@
 	QueryStats(int) ([]db.Stats, error)
 	QueryNotes(int32) ([]string, error)
 	AddNotes(db.NotesData) error
+	AddDriverRanking(db.DriverRankingData) error
 }
 
 type ScrapeMatchList func(int32, string) ([]scraping.Match, error)
@@ -608,6 +613,40 @@
 	w.Write(builder.FinishedBytes())
 }
 
+type SubmitDriverRankingHandler struct {
+	db Database
+}
+
+func (handler SubmitDriverRankingHandler) ServeHTTP(w http.ResponseWriter, req *http.Request) {
+	requestBytes, err := io.ReadAll(req.Body)
+	if err != nil {
+		respondWithError(w, http.StatusBadRequest, fmt.Sprint("Failed to read request bytes:", err))
+		return
+	}
+
+	request, success := parseRequest(w, requestBytes, "SubmitDriverRanking", submit_driver_ranking.GetRootAsSubmitDriverRanking)
+	if !success {
+		return
+	}
+
+	err = handler.db.AddDriverRanking(db.DriverRankingData{
+		MatchNumber: request.MatchNumber(),
+		Rank1:       request.Rank1(),
+		Rank2:       request.Rank2(),
+		Rank3:       request.Rank3(),
+	})
+
+	if err != nil {
+		respondWithError(w, http.StatusInternalServerError, fmt.Sprintf("Failed to insert driver ranking: %v", err))
+		return
+	}
+
+	var response SubmitDriverRankingResponseT
+	builder := flatbuffers.NewBuilder(10)
+	builder.Finish((&response).Pack(builder))
+	w.Write(builder.FinishedBytes())
+}
+
 func HandleRequests(db Database, scrape ScrapeMatchList, scoutingServer server.ScoutingServer) {
 	scoutingServer.HandleFunc("/requests", unknown)
 	scoutingServer.Handle("/requests/submit/data_scouting", submitDataScoutingHandler{db})
@@ -619,4 +658,5 @@
 	scoutingServer.Handle("/requests/request/notes_for_team", requestNotesForTeamHandler{db})
 	scoutingServer.Handle("/requests/submit/shift_schedule", submitShiftScheduleHandler{db})
 	scoutingServer.Handle("/requests/request/shift_schedule", requestShiftScheduleHandler{db})
+	scoutingServer.Handle("/requests/submit/submit_driver_ranking", SubmitDriverRankingHandler{db})
 }
diff --git a/scouting/webserver/requests/requests_test.go b/scouting/webserver/requests/requests_test.go
index 85ab916..55b789b 100644
--- a/scouting/webserver/requests/requests_test.go
+++ b/scouting/webserver/requests/requests_test.go
@@ -24,6 +24,7 @@
 	"github.com/frc971/971-Robot-Code/scouting/webserver/requests/messages/request_shift_schedule_response"
 	"github.com/frc971/971-Robot-Code/scouting/webserver/requests/messages/submit_data_scouting"
 	"github.com/frc971/971-Robot-Code/scouting/webserver/requests/messages/submit_data_scouting_response"
+	"github.com/frc971/971-Robot-Code/scouting/webserver/requests/messages/submit_driver_ranking"
 	"github.com/frc971/971-Robot-Code/scouting/webserver/requests/messages/submit_notes"
 	"github.com/frc971/971-Robot-Code/scouting/webserver/requests/messages/submit_shift_schedule"
 	"github.com/frc971/971-Robot-Code/scouting/webserver/server"
@@ -560,14 +561,44 @@
 	}
 }
 
+func TestSubmitDriverRanking(t *testing.T) {
+	database := MockDatabase{}
+	scoutingServer := server.NewScoutingServer()
+	HandleRequests(&database, scrapeEmtpyMatchList, scoutingServer)
+	scoutingServer.Start(8080)
+	defer scoutingServer.Stop()
+
+	builder := flatbuffers.NewBuilder(1024)
+	builder.Finish((&submit_driver_ranking.SubmitDriverRankingT{
+		MatchNumber: 36,
+		Rank1:       1234,
+		Rank2:       1235,
+		Rank3:       1236,
+	}).Pack(builder))
+
+	_, err := debug.SubmitDriverRanking("http://localhost:8080", builder.FinishedBytes())
+	if err != nil {
+		t.Fatal("Failed to submit driver ranking: ", err)
+	}
+
+	expected := []db.DriverRankingData{
+		{MatchNumber: 36, Rank1: 1234, Rank2: 1235, Rank3: 1236},
+	}
+
+	if !reflect.DeepEqual(database.driver_ranking, expected) {
+		t.Fatal("Submitted notes did not match", expected, database.notes)
+	}
+}
+
 // A mocked database we can use for testing. Add functionality to this as
 // needed for your tests.
 
 type MockDatabase struct {
-	matches       []db.Match
-	stats         []db.Stats
-	notes         []db.NotesData
-	shiftSchedule []db.Shift
+	matches        []db.Match
+	stats          []db.Stats
+	notes          []db.NotesData
+	shiftSchedule  []db.Shift
+	driver_ranking []db.DriverRankingData
 }
 
 func (database *MockDatabase) AddToMatch(match db.Match) error {
@@ -633,6 +664,11 @@
 	return []db.Shift{}, nil
 }
 
+func (database *MockDatabase) AddDriverRanking(data db.DriverRankingData) error {
+	database.driver_ranking = append(database.driver_ranking, data)
+	return nil
+}
+
 // Returns an empty match list from the fake The Blue Alliance scraping.
 func scrapeEmtpyMatchList(int32, string) ([]scraping.Match, error) {
 	return nil, nil
diff --git a/scouting/www/BUILD b/scouting/www/BUILD
index 0b7cebb..ee0659b 100644
--- a/scouting/www/BUILD
+++ b/scouting/www/BUILD
@@ -16,6 +16,7 @@
     use_angular_plugin = True,
     visibility = ["//visibility:public"],
     deps = [
+        "//scouting/www/driver_ranking",
         "//scouting/www/entry",
         "//scouting/www/import_match_list",
         "//scouting/www/match_list",
diff --git a/scouting/www/app.ng.html b/scouting/www/app.ng.html
index 297fd39..d9dbead 100644
--- a/scouting/www/app.ng.html
+++ b/scouting/www/app.ng.html
@@ -40,6 +40,15 @@
   <li class="nav-item">
     <a
       class="nav-link"
+      [class.active]="tabIs('DriverRanking')"
+      (click)="switchTabToGuarded('DriverRanking')"
+    >
+      Driver Ranking
+    </a>
+  </li>
+  <li class="nav-item">
+    <a
+      class="nav-link"
       [class.active]="tabIs('ImportMatchList')"
       (click)="switchTabToGuarded('ImportMatchList')"
     >
@@ -80,6 +89,7 @@
     *ngSwitchCase="'Entry'"
   ></app-entry>
   <frc971-notes *ngSwitchCase="'Notes'"></frc971-notes>
+  <app-driver-ranking *ngSwitchCase="'DriverRanking'"></app-driver-ranking>
   <app-import-match-list
     *ngSwitchCase="'ImportMatchList'"
   ></app-import-match-list>
diff --git a/scouting/www/app.ts b/scouting/www/app.ts
index 4f95c90..b26f815 100644
--- a/scouting/www/app.ts
+++ b/scouting/www/app.ts
@@ -4,6 +4,7 @@
   | 'MatchList'
   | 'Notes'
   | 'Entry'
+  | 'DriverRanking'
   | 'ImportMatchList'
   | 'ShiftSchedule'
   | 'View';
diff --git a/scouting/www/app_module.ts b/scouting/www/app_module.ts
index 8c18f7a..04d72b3 100644
--- a/scouting/www/app_module.ts
+++ b/scouting/www/app_module.ts
@@ -9,6 +9,7 @@
 import {NotesModule} from './notes/notes.module';
 import {ShiftScheduleModule} from './shift_schedule/shift_schedule.module';
 import {ViewModule} from './view/view.module';
+import {DriverRankingModule} from './driver_ranking/driver_ranking.module';
 
 @NgModule({
   declarations: [App],
@@ -20,6 +21,7 @@
     ImportMatchListModule,
     MatchListModule,
     ShiftScheduleModule,
+    DriverRankingModule,
     ViewModule,
   ],
   exports: [App],
diff --git a/scouting/www/driver_ranking/BUILD b/scouting/www/driver_ranking/BUILD
new file mode 100644
index 0000000..10b6f99
--- /dev/null
+++ b/scouting/www/driver_ranking/BUILD
@@ -0,0 +1,26 @@
+load("@npm//@bazel/typescript:index.bzl", "ts_library")
+
+ts_library(
+    name = "driver_ranking",
+    srcs = [
+        "driver_ranking.component.ts",
+        "driver_ranking.module.ts",
+    ],
+    angular_assets = [
+        "driver_ranking.component.css",
+        "driver_ranking.ng.html",
+        "//scouting/www:common_css",
+    ],
+    compiler = "//tools:tsc_wrapped_with_angular",
+    target_compatible_with = ["@platforms//cpu:x86_64"],
+    use_angular_plugin = True,
+    visibility = ["//visibility:public"],
+    deps = [
+        "//scouting/webserver/requests/messages:error_response_ts_fbs",
+        "//scouting/webserver/requests/messages:submit_driver_ranking_ts_fbs",
+        "@com_github_google_flatbuffers//ts:flatbuffers_ts",
+        "@npm//@angular/common",
+        "@npm//@angular/core",
+        "@npm//@angular/forms",
+    ],
+)
diff --git a/scouting/www/driver_ranking/driver_ranking.component.css b/scouting/www/driver_ranking/driver_ranking.component.css
new file mode 100644
index 0000000..e220645
--- /dev/null
+++ b/scouting/www/driver_ranking/driver_ranking.component.css
@@ -0,0 +1,3 @@
+* {
+  padding: 10px;
+}
diff --git a/scouting/www/driver_ranking/driver_ranking.component.ts b/scouting/www/driver_ranking/driver_ranking.component.ts
new file mode 100644
index 0000000..aadb3b0
--- /dev/null
+++ b/scouting/www/driver_ranking/driver_ranking.component.ts
@@ -0,0 +1,93 @@
+import {Component, OnInit} from '@angular/core';
+import {Builder, ByteBuffer} from 'flatbuffers';
+import {SubmitDriverRanking} from 'org_frc971/scouting/webserver/requests/messages/submit_driver_ranking_generated';
+import {ErrorResponse} from 'org_frc971/scouting/webserver/requests/messages/error_response_generated';
+
+// TeamSelection: Display form to input which
+// teams to rank and the match number.
+// Data: Display the ranking interface where
+// the scout can reorder teams and submit data.
+type Section = 'TeamSelection' | 'Data';
+
+@Component({
+  selector: 'app-driver-ranking',
+  templateUrl: './driver_ranking.ng.html',
+  styleUrls: ['../common.css', './driver_ranking.component.css'],
+})
+export class DriverRankingComponent {
+  section: Section = 'TeamSelection';
+
+  // Stores the team keys and rank (order of the array).
+  team_ranking: number[] = [971, 972, 973];
+
+  match_number: number = 1;
+
+  errorMessage = '';
+
+  setTeamNumbers() {
+    this.section = 'Data';
+  }
+
+  rankUp(index: number) {
+    if (index > 0) {
+      this.changeRank(index, index - 1);
+    }
+  }
+
+  rankDown(index: number) {
+    if (index < 2) {
+      this.changeRank(index, index + 1);
+    }
+  }
+
+  // Change the rank of a team in team_ranking.
+  // Move the the team at index 'fromIndex'
+  // to the index 'toIndex'.
+  // Ex. Moving the rank 2 (index 1) team to rank1 (index 0)
+  // would be changeRank(1, 0)
+
+  changeRank(fromIndex: number, toIndex: number) {
+    var element = this.team_ranking[fromIndex];
+    this.team_ranking.splice(fromIndex, 1);
+    this.team_ranking.splice(toIndex, 0, element);
+  }
+
+  editTeams() {
+    this.section = 'TeamSelection';
+  }
+
+  async submitData() {
+    const builder = new Builder();
+    builder.finish(
+      SubmitDriverRanking.createSubmitDriverRanking(
+        builder,
+        this.match_number,
+        this.team_ranking[0],
+        this.team_ranking[1],
+        this.team_ranking[2]
+      )
+    );
+    const buffer = builder.asUint8Array();
+    const res = await fetch('/requests/submit/submit_driver_ranking', {
+      method: 'POST',
+      body: buffer,
+    });
+
+    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}"`;
+      return;
+    }
+
+    // Increment the match number.
+    this.match_number = this.match_number + 1;
+
+    // Reset Data.
+    this.section = 'TeamSelection';
+    this.team_ranking = [971, 972, 973];
+  }
+}
diff --git a/scouting/www/driver_ranking/driver_ranking.module.ts b/scouting/www/driver_ranking/driver_ranking.module.ts
new file mode 100644
index 0000000..7fe3623
--- /dev/null
+++ b/scouting/www/driver_ranking/driver_ranking.module.ts
@@ -0,0 +1,11 @@
+import {CommonModule} from '@angular/common';
+import {NgModule} from '@angular/core';
+import {FormsModule} from '@angular/forms';
+import {DriverRankingComponent} from './driver_ranking.component';
+
+@NgModule({
+  declarations: [DriverRankingComponent],
+  exports: [DriverRankingComponent],
+  imports: [CommonModule, FormsModule],
+})
+export class DriverRankingModule {}
diff --git a/scouting/www/driver_ranking/driver_ranking.ng.html b/scouting/www/driver_ranking/driver_ranking.ng.html
new file mode 100644
index 0000000..21aeec1
--- /dev/null
+++ b/scouting/www/driver_ranking/driver_ranking.ng.html
@@ -0,0 +1,51 @@
+<h2>Driver Ranking</h2>
+
+<ng-container [ngSwitch]="section">
+  <div *ngSwitchCase="'TeamSelection'">
+    <label for="match_number_selection">Match Number</label>
+    <input
+      [(ngModel)]="match_number"
+      type="number"
+      id="match_number_selection"
+      min="1"
+      max="9999"
+    />
+    <br />
+    <br />
+    <label>Team Numbers</label>
+    <input
+      *ngFor="let x of [1,2,3]; let i = index;"
+      [(ngModel)]="team_ranking[i]"
+      type="number"
+      min="1"
+      max="9999"
+    />
+    <button class="btn btn-primary" (click)="setTeamNumbers()">Select</button>
+  </div>
+  <div *ngSwitchCase="'Data'">
+    <h4>Match #{{match_number}}</h4>
+    <div *ngFor="let team_key of team_ranking; let i = index">
+      <div class="d-flex flex-row justify-content-center pt-2">
+        <div class="d-flex flex-row">
+          <h4 class="align-self-center">{{i + 1}}</h4>
+          <h1 class="align-self-center">{{team_key}}</h1>
+        </div>
+        <button class="btn btn-success" (click)="rankUp(i)">&uarr;</button>
+        <!--&uarr; is the html code for an up arrow-->
+        <button class="btn btn-danger" (click)="rankDown(i)">&darr;</button>
+        <!--&darr; is the html code for a down arrow-->
+      </div>
+    </div>
+    <div class="d-flex flex-row justify-content-center pt-2">
+      <div>
+        <button class="btn btn-secondary" (click)="editTeams()">
+          Edit Teams
+        </button>
+      </div>
+      <div>
+        <button class="btn btn-success" (click)="submitData()">Submit</button>
+      </div>
+    </div>
+  </div>
+  <div class="error">{{errorMessage}}</div>
+</ng-container>