Merge "Fix auto-aiming when turret range doesn't include [-pi, +pi]"
diff --git a/scouting/db/db.go b/scouting/db/db.go
index 98cc788..d9bebc9 100644
--- a/scouting/db/db.go
+++ b/scouting/db/db.go
@@ -21,7 +21,10 @@
 }
 
 type Stats struct {
-	TeamNumber, MatchNumber                                      int32
+	TeamNumber, MatchNumber int32
+	StartingQuadrant        int32
+	AutoBallPickedUp        [5]bool
+	// TODO(phil): Re-order auto and teleop fields so auto comes first.
 	ShotsMissed, UpperGoalShots, LowerGoalShots                  int32
 	ShotsMissedAuto, UpperGoalAuto, LowerGoalAuto, PlayedDefense int32
 	Climbing                                                     int32
@@ -80,6 +83,12 @@
 		"id SERIAL PRIMARY KEY, " +
 		"TeamNumber INTEGER, " +
 		"MatchNumber INTEGER, " +
+		"StartingQuadrant INTEGER, " +
+		"AutoBall1PickedUp BOOLEAN, " +
+		"AutoBall2PickedUp BOOLEAN, " +
+		"AutoBall3PickedUp BOOLEAN, " +
+		"AutoBall4PickedUp BOOLEAN, " +
+		"AutoBall5PickedUp BOOLEAN, " +
 		"ShotsMissed INTEGER, " +
 		"UpperGoalShots INTEGER, " +
 		"LowerGoalShots INTEGER, " +
@@ -152,14 +161,20 @@
 func (database *Database) AddToMatch(m Match) error {
 	statement, err := database.Prepare("INSERT INTO team_match_stats(" +
 		"TeamNumber, MatchNumber, " +
+		"StartingQuadrant, " +
+		"AutoBall1PickedUp, AutoBall2PickedUp, AutoBall3PickedUp, " +
+		"AutoBall4PickedUp, AutoBall5PickedUp, " +
 		"ShotsMissed, UpperGoalShots, LowerGoalShots, " +
 		"ShotsMissedAuto, UpperGoalAuto, LowerGoalAuto, " +
 		"PlayedDefense, Climbing, CollectedBy) " +
 		"VALUES (" +
 		"$1, $2, " +
-		"$3, $4, $5, " +
-		"$6, $7, $8, " +
-		"$9, $10, $11) " +
+		"$3, " +
+		"$4, $5, $6, " +
+		"$7, $8, " +
+		"$9, $10, $11, " +
+		"$12, $13, $14, " +
+		"$15, $16, $17) " +
 		"RETURNING id")
 	if err != nil {
 		return errors.New(fmt.Sprint("Failed to prepare insertion into stats database: ", err))
@@ -168,7 +183,14 @@
 
 	var rowIds [6]int64
 	for i, TeamNumber := range []int32{m.R1, m.R2, m.R3, m.B1, m.B2, m.B3} {
-		row := statement.QueryRow(TeamNumber, m.MatchNumber, 0, 0, 0, 0, 0, 0, 0, 0, "")
+		row := statement.QueryRow(
+			TeamNumber, m.MatchNumber,
+			0,
+			false, false, false,
+			false, false,
+			0, 0, 0,
+			0, 0, 0,
+			0, 0, "")
 		err = row.Scan(&rowIds[i])
 		if err != nil {
 			return errors.New(fmt.Sprint("Failed to insert stats: ", err))
@@ -200,16 +222,23 @@
 func (database *Database) AddToStats(s Stats) error {
 	statement, err := database.Prepare("UPDATE team_match_stats SET " +
 		"TeamNumber = $1, MatchNumber = $2, " +
-		"ShotsMissed = $3, UpperGoalShots = $4, LowerGoalShots = $5, " +
-		"ShotsMissedAuto = $6, UpperGoalAuto = $7, LowerGoalAuto = $8, " +
-		"PlayedDefense = $9, Climbing = $10, CollectedBy = $11 " +
-		"WHERE MatchNumber = $12 AND TeamNumber = $13")
+		"StartingQuadrant = $3, " +
+		"AutoBall1PickedUp = $4, AutoBall2PickedUp = $5, AutoBall3PickedUp = $6, " +
+		"AutoBall4PickedUp = $7, AutoBall5PickedUp = $8, " +
+		"ShotsMissed = $9, UpperGoalShots = $10, LowerGoalShots = $11, " +
+		"ShotsMissedAuto = $12, UpperGoalAuto = $13, LowerGoalAuto = $14, " +
+		"PlayedDefense = $15, Climbing = $16, CollectedBy = $17 " +
+		"WHERE MatchNumber = $18 AND TeamNumber = $19")
 	if err != nil {
 		return errors.New(fmt.Sprint("Failed to prepare stats update statement: ", err))
 	}
 	defer statement.Close()
 
-	result, err := statement.Exec(s.TeamNumber, s.MatchNumber,
+	result, err := statement.Exec(
+		s.TeamNumber, s.MatchNumber,
+		s.StartingQuadrant,
+		s.AutoBallPickedUp[0], s.AutoBallPickedUp[1], s.AutoBallPickedUp[2],
+		s.AutoBallPickedUp[3], s.AutoBallPickedUp[4],
 		s.ShotsMissed, s.UpperGoalShots, s.LowerGoalShots,
 		s.ShotsMissedAuto, s.UpperGoalAuto, s.LowerGoalAuto,
 		s.PlayedDefense, s.Climbing, s.CollectedBy,
@@ -263,7 +292,11 @@
 	for rows.Next() {
 		var team Stats
 		var id int
-		err = rows.Scan(&id, &team.TeamNumber, &team.MatchNumber,
+		err = rows.Scan(&id,
+			&team.TeamNumber, &team.MatchNumber,
+			&team.StartingQuadrant,
+			&team.AutoBallPickedUp[0], &team.AutoBallPickedUp[1], &team.AutoBallPickedUp[2],
+			&team.AutoBallPickedUp[3], &team.AutoBallPickedUp[4],
 			&team.ShotsMissed, &team.UpperGoalShots, &team.LowerGoalShots,
 			&team.ShotsMissedAuto, &team.UpperGoalAuto, &team.LowerGoalAuto,
 			&team.PlayedDefense, &team.Climbing, &team.CollectedBy)
@@ -310,7 +343,11 @@
 	for rows.Next() {
 		var team Stats
 		var id int
-		err = rows.Scan(&id, &team.TeamNumber, &team.MatchNumber,
+		err = rows.Scan(&id,
+			&team.TeamNumber, &team.MatchNumber,
+			&team.StartingQuadrant,
+			&team.AutoBallPickedUp[0], &team.AutoBallPickedUp[1], &team.AutoBallPickedUp[2],
+			&team.AutoBallPickedUp[3], &team.AutoBallPickedUp[4],
 			&team.ShotsMissed, &team.UpperGoalShots, &team.LowerGoalShots,
 			&team.ShotsMissedAuto, &team.UpperGoalAuto, &team.LowerGoalAuto,
 			&team.PlayedDefense, &team.Climbing, &team.CollectedBy)
diff --git a/scouting/db/db_test.go b/scouting/db/db_test.go
index b06706d..c5bea5e 100644
--- a/scouting/db/db_test.go
+++ b/scouting/db/db_test.go
@@ -89,42 +89,54 @@
 	correct := []Stats{
 		Stats{
 			TeamNumber: 1236, MatchNumber: 7,
-			ShotsMissed: 9, UpperGoalShots: 5, LowerGoalShots: 4,
+			StartingQuadrant: 2,
+			AutoBallPickedUp: [5]bool{false, false, false, true, false},
+			ShotsMissed:      9, UpperGoalShots: 5, LowerGoalShots: 4,
 			ShotsMissedAuto: 3, UpperGoalAuto: 2, LowerGoalAuto: 1,
 			PlayedDefense: 2, Climbing: 3,
 			CollectedBy: "josh",
 		},
 		Stats{
 			TeamNumber: 1001, MatchNumber: 7,
-			ShotsMissed: 6, UpperGoalShots: 9, LowerGoalShots: 9,
+			StartingQuadrant: 3,
+			AutoBallPickedUp: [5]bool{true, false, true, true, false},
+			ShotsMissed:      6, UpperGoalShots: 9, LowerGoalShots: 9,
 			ShotsMissedAuto: 0, UpperGoalAuto: 0, LowerGoalAuto: 0,
 			PlayedDefense: 0, Climbing: 0,
 			CollectedBy: "rupert",
 		},
 		Stats{
 			TeamNumber: 777, MatchNumber: 7,
-			ShotsMissed: 5, UpperGoalShots: 7, LowerGoalShots: 12,
+			StartingQuadrant: 4,
+			AutoBallPickedUp: [5]bool{false, true, true, true, false},
+			ShotsMissed:      5, UpperGoalShots: 7, LowerGoalShots: 12,
 			ShotsMissedAuto: 0, UpperGoalAuto: 4, LowerGoalAuto: 0,
 			PlayedDefense: 0, Climbing: 0,
 			CollectedBy: "felix",
 		},
 		Stats{
 			TeamNumber: 1000, MatchNumber: 7,
-			ShotsMissed: 12, UpperGoalShots: 6, LowerGoalShots: 10,
+			StartingQuadrant: 1,
+			AutoBallPickedUp: [5]bool{false, false, false, false, false},
+			ShotsMissed:      12, UpperGoalShots: 6, LowerGoalShots: 10,
 			ShotsMissedAuto: 0, UpperGoalAuto: 7, LowerGoalAuto: 0,
 			PlayedDefense: 0, Climbing: 0,
 			CollectedBy: "thea",
 		},
 		Stats{
 			TeamNumber: 4321, MatchNumber: 7,
-			ShotsMissed: 14, UpperGoalShots: 12, LowerGoalShots: 3,
+			StartingQuadrant: 2,
+			AutoBallPickedUp: [5]bool{true, false, false, false, false},
+			ShotsMissed:      14, UpperGoalShots: 12, LowerGoalShots: 3,
 			ShotsMissedAuto: 0, UpperGoalAuto: 7, LowerGoalAuto: 0,
 			PlayedDefense: 0, Climbing: 0,
 			CollectedBy: "amy",
 		},
 		Stats{
 			TeamNumber: 1234, MatchNumber: 7,
-			ShotsMissed: 3, UpperGoalShots: 4, LowerGoalShots: 0,
+			StartingQuadrant: 3,
+			AutoBallPickedUp: [5]bool{false, false, false, false, true},
+			ShotsMissed:      3, UpperGoalShots: 4, LowerGoalShots: 0,
 			ShotsMissedAuto: 0, UpperGoalAuto: 9, LowerGoalAuto: 0,
 			PlayedDefense: 0, Climbing: 0,
 			CollectedBy: "beth",
@@ -195,36 +207,48 @@
 	testDatabase := []Stats{
 		Stats{
 			TeamNumber: 1235, MatchNumber: 94,
-			ShotsMissed: 2, UpperGoalShots: 2, LowerGoalShots: 2,
+			StartingQuadrant: 1,
+			AutoBallPickedUp: [5]bool{false, false, false, false, false},
+			ShotsMissed:      2, UpperGoalShots: 2, LowerGoalShots: 2,
 			ShotsMissedAuto: 2, UpperGoalAuto: 2, LowerGoalAuto: 2,
 			PlayedDefense: 2, Climbing: 2},
 		Stats{
 			TeamNumber: 1234, MatchNumber: 94,
-			ShotsMissed: 4, UpperGoalShots: 4, LowerGoalShots: 4,
+			StartingQuadrant: 2,
+			AutoBallPickedUp: [5]bool{false, false, false, false, true},
+			ShotsMissed:      4, UpperGoalShots: 4, LowerGoalShots: 4,
 			ShotsMissedAuto: 4, UpperGoalAuto: 4, LowerGoalAuto: 4,
 			PlayedDefense: 7, Climbing: 2,
 		},
 		Stats{
 			TeamNumber: 1233, MatchNumber: 94,
-			ShotsMissed: 3, UpperGoalShots: 3, LowerGoalShots: 3,
+			StartingQuadrant: 3,
+			AutoBallPickedUp: [5]bool{false, false, false, false, false},
+			ShotsMissed:      3, UpperGoalShots: 3, LowerGoalShots: 3,
 			ShotsMissedAuto: 3, UpperGoalAuto: 3, LowerGoalAuto: 3,
 			PlayedDefense: 3, Climbing: 3,
 		},
 		Stats{
 			TeamNumber: 1232, MatchNumber: 94,
-			ShotsMissed: 5, UpperGoalShots: 5, LowerGoalShots: 5,
+			StartingQuadrant: 2,
+			AutoBallPickedUp: [5]bool{true, false, false, false, true},
+			ShotsMissed:      5, UpperGoalShots: 5, LowerGoalShots: 5,
 			ShotsMissedAuto: 5, UpperGoalAuto: 5, LowerGoalAuto: 5,
 			PlayedDefense: 7, Climbing: 1,
 		},
 		Stats{
 			TeamNumber: 1231, MatchNumber: 94,
-			ShotsMissed: 6, UpperGoalShots: 6, LowerGoalShots: 6,
+			StartingQuadrant: 3,
+			AutoBallPickedUp: [5]bool{false, false, true, false, false},
+			ShotsMissed:      6, UpperGoalShots: 6, LowerGoalShots: 6,
 			ShotsMissedAuto: 6, UpperGoalAuto: 6, LowerGoalAuto: 6,
 			PlayedDefense: 7, Climbing: 1,
 		},
 		Stats{
 			TeamNumber: 1239, MatchNumber: 94,
-			ShotsMissed: 7, UpperGoalShots: 7, LowerGoalShots: 7,
+			StartingQuadrant: 4,
+			AutoBallPickedUp: [5]bool{false, true, true, false, false},
+			ShotsMissed:      7, UpperGoalShots: 7, LowerGoalShots: 7,
 			ShotsMissedAuto: 7, UpperGoalAuto: 7, LowerGoalAuto: 3,
 			PlayedDefense: 7, Climbing: 1,
 		},
@@ -237,13 +261,15 @@
 
 	for i := 0; i < len(testDatabase); i++ {
 		err = fixture.db.AddToStats(testDatabase[i])
-		check(t, err, fmt.Sprint("Failed to add stats", i))
+		check(t, err, fmt.Sprint("Failed to add stats ", i))
 	}
 
 	correct := []Stats{
 		Stats{
 			TeamNumber: 1235, MatchNumber: 94,
-			ShotsMissed: 2, UpperGoalShots: 2, LowerGoalShots: 2,
+			StartingQuadrant: 1,
+			AutoBallPickedUp: [5]bool{false, false, false, false, false},
+			ShotsMissed:      2, UpperGoalShots: 2, LowerGoalShots: 2,
 			ShotsMissedAuto: 2, UpperGoalAuto: 2, LowerGoalAuto: 2,
 			PlayedDefense: 2, Climbing: 2,
 		},
@@ -309,37 +335,48 @@
 	correct := []Stats{
 		Stats{
 			TeamNumber: 1235, MatchNumber: 94,
-			ShotsMissed: 2, UpperGoalShots: 2, LowerGoalShots: 2,
+			StartingQuadrant: 1,
+			AutoBallPickedUp: [5]bool{false, false, false, false, false},
+			ShotsMissed:      2, UpperGoalShots: 2, LowerGoalShots: 2,
 			ShotsMissedAuto: 2, UpperGoalAuto: 2, LowerGoalAuto: 2,
-			PlayedDefense: 2, Climbing: 2,
-		},
+			PlayedDefense: 2, Climbing: 2},
 		Stats{
 			TeamNumber: 1236, MatchNumber: 94,
-			ShotsMissed: 4, UpperGoalShots: 4, LowerGoalShots: 4,
+			StartingQuadrant: 2,
+			AutoBallPickedUp: [5]bool{false, false, false, false, true},
+			ShotsMissed:      4, UpperGoalShots: 4, LowerGoalShots: 4,
 			ShotsMissedAuto: 4, UpperGoalAuto: 4, LowerGoalAuto: 4,
 			PlayedDefense: 7, Climbing: 2,
 		},
 		Stats{
 			TeamNumber: 1237, MatchNumber: 94,
-			ShotsMissed: 3, UpperGoalShots: 3, LowerGoalShots: 3,
+			StartingQuadrant: 3,
+			AutoBallPickedUp: [5]bool{false, false, false, false, false},
+			ShotsMissed:      3, UpperGoalShots: 3, LowerGoalShots: 3,
 			ShotsMissedAuto: 3, UpperGoalAuto: 3, LowerGoalAuto: 3,
 			PlayedDefense: 3, Climbing: 3,
 		},
 		Stats{
 			TeamNumber: 1238, MatchNumber: 94,
-			ShotsMissed: 5, UpperGoalShots: 5, LowerGoalShots: 5,
+			StartingQuadrant: 2,
+			AutoBallPickedUp: [5]bool{true, false, false, false, true},
+			ShotsMissed:      5, UpperGoalShots: 5, LowerGoalShots: 5,
 			ShotsMissedAuto: 5, UpperGoalAuto: 5, LowerGoalAuto: 5,
 			PlayedDefense: 7, Climbing: 1,
 		},
 		Stats{
 			TeamNumber: 1239, MatchNumber: 94,
-			ShotsMissed: 6, UpperGoalShots: 6, LowerGoalShots: 6,
+			StartingQuadrant: 3,
+			AutoBallPickedUp: [5]bool{false, false, true, false, false},
+			ShotsMissed:      6, UpperGoalShots: 6, LowerGoalShots: 6,
 			ShotsMissedAuto: 6, UpperGoalAuto: 6, LowerGoalAuto: 6,
 			PlayedDefense: 7, Climbing: 1,
 		},
 		Stats{
 			TeamNumber: 1233, MatchNumber: 94,
-			ShotsMissed: 7, UpperGoalShots: 7, LowerGoalShots: 7,
+			StartingQuadrant: 4,
+			AutoBallPickedUp: [5]bool{false, true, true, false, false},
+			ShotsMissed:      7, UpperGoalShots: 7, LowerGoalShots: 7,
 			ShotsMissedAuto: 7, UpperGoalAuto: 7, LowerGoalAuto: 3,
 			PlayedDefense: 7, Climbing: 1,
 		},
@@ -352,7 +389,7 @@
 
 	for i := 0; i < len(correct); i++ {
 		err = fixture.db.AddToStats(correct[i])
-		check(t, err, fmt.Sprint("Failed to add stats", i))
+		check(t, err, fmt.Sprint("Failed to add stats ", i))
 	}
 
 	got, err := fixture.db.ReturnStats()
diff --git a/scouting/scouting_test.ts b/scouting/scouting_test.ts
index db0c9fd..84824ff 100644
--- a/scouting/scouting_test.ts
+++ b/scouting/scouting_test.ts
@@ -1,22 +1,39 @@
 import {browser, by, element, protractor} from 'protractor';
 
+const EC = protractor.ExpectedConditions;
+
 // Loads the page (or reloads it) and deals with the "Are you sure you want to
 // leave this page" popup.
 async function loadPage() {
-  await browser.get(browser.baseUrl).catch(function () {
-    return browser.switchTo().alert().then(function (alert) {
-      alert.accept();
-      return browser.get(browser.baseUrl);
-    });
-  });
+  await disableAlerts();
+  await browser.navigate().refresh();
+  expect((await browser.getTitle())).toEqual('FRC971 Scouting Application');
+  await disableAlerts();
 }
 
+// Disables alert popups. They are extremely tedious to deal with in
+// Protractor since they're not angular elements. We achieve this by checking
+// an invisible checkbox that's off-screen.
+async function disableAlerts() {
+  await browser.executeAsyncScript(function(callback) {
+    let block_alerts =
+        document.getElementById('block_alerts') as HTMLInputElement;
+    block_alerts.checked = true;
+    callback();
+  });
+}
 // Returns the contents of the header that displays the "Auto", "TeleOp", and
 // "Climb" labels etc.
 function getHeadingText() {
   return element(by.css('.header')).getText();
 }
 
+// Returns the currently displayed progress message on the screen. This only
+// exists on screens where the web page interacts with the web server.
+function getProgressMessage() {
+  return element(by.css('.progress_message')).getText();
+}
+
 // Returns the currently displayed error message on the screen. This only
 // exists on screens where the web page interacts with the web server.
 function getErrorMessage() {
@@ -31,25 +48,79 @@
 
 // Asserts that the n'th instance of a field on the "Submit and Review"
 // screen has a specific value.
-async function expectNthReviewFieldToBe(fieldName: string, n: number, expectedValue: string) {
-  expect(await element.all(by.cssContainingText('li', `${fieldName}:`)).get(n).getText())
+async function expectNthReviewFieldToBe(
+    fieldName: string, n: number, expectedValue: string) {
+  expect(await element.all(by.cssContainingText('li', `${fieldName}:`))
+             .get(n)
+             .getText())
       .toEqual(`${fieldName}: ${expectedValue}`);
 }
 
+// Sets a text field to the specified value.
+function setTextboxByIdTo(id: string, value: string) {
+  // Just sending "value" to the input fields is insufficient. We need to
+  // overwrite the text that is there. If we didn't hit CTRL-A to select all
+  // the text, we'd be appending to whatever is there already.
+  return element(by.id(id)).sendKeys(
+      protractor.Key.CONTROL, 'a', protractor.Key.NULL, value);
+}
+
 describe('The scouting web page', () => {
+  beforeAll(async () => {
+    await browser.get(browser.baseUrl);
+    expect((await browser.getTitle())).toEqual('FRC971 Scouting Application');
+    await disableAlerts();
+
+    // Import the match list before running any tests. Ideally this should be
+    // run in beforeEach(), but it's not worth doing that at this time. Our
+    // tests are basic enough not to require this.
+    await element(by.cssContainingText('.nav-link', 'Import Match List'))
+        .click();
+    expect(await getHeadingText()).toEqual('Import Match List');
+    await setTextboxByIdTo('year', '2016');
+    await setTextboxByIdTo('event_code', 'nytr');
+    await element(by.buttonText('Import')).click();
+
+    await browser.wait(EC.textToBePresentInElement(
+        element(by.css('.progress_message')),
+        'Successfully imported match list.'));
+  });
+
+  it('should: error on unknown match.', async () => {
+    await loadPage();
+
+    await element(by.cssContainingText('.nav-link', 'Data Entry')).click();
+
+    // Pick a match that doesn't exist in the 2016nytr match list.
+    await setTextboxByIdTo('match_number', '3');
+    await setTextboxByIdTo('team_number', '971');
+
+    // Click Next until we get to the submit screen.
+    for (let i = 0; i < 5; i++) {
+      await element(by.buttonText('Next')).click();
+    }
+    expect(await getHeadingText()).toEqual('Review and Submit');
+
+    // Attempt to submit and validate the error.
+    await element(by.buttonText('Submit')).click();
+    expect(await getErrorMessage())
+        .toContain('Failed to find team 971 in match 3 in the schedule.');
+  });
+
+
   it('should: review and submit correct data.', async () => {
     await loadPage();
 
+    await element(by.cssContainingText('.nav-link', 'Data Entry')).click();
+
+    // Submit scouting data for a random team that attended 2016nytr.
     expect(await getHeadingText()).toEqual('Team Selection');
-    // Just sending "971" to the input fields is insufficient. We need to
-    // overwrite the text that is there. If we didn't hit CTRL-A to select all
-    // the text, we'd be appending to whatever is there already.
-    await element(by.id('team_number')).sendKeys(
-        protractor.Key.CONTROL, 'a', protractor.Key.NULL,
-        '971');
+    await setTextboxByIdTo('match_number', '2');
+    await setTextboxByIdTo('team_number', '5254');
     await element(by.buttonText('Next')).click();
 
     expect(await getHeadingText()).toEqual('Auto');
+    await element(by.id('quadrant3')).click();
     await element(by.buttonText('Next')).click();
 
     expect(await getHeadingText()).toEqual('TeleOp');
@@ -68,13 +139,14 @@
     expect(await getErrorMessage()).toEqual('');
 
     // Validate Team Selection.
-    await expectReviewFieldToBe('Match number', '1');
-    await expectReviewFieldToBe('Team number', '971');
+    await expectReviewFieldToBe('Match number', '2');
+    await expectReviewFieldToBe('Team number', '5254');
 
     // Validate Auto.
     await expectNthReviewFieldToBe('Upper Shots Made', 0, '0');
     await expectNthReviewFieldToBe('Lower Shots Made', 0, '0');
     await expectNthReviewFieldToBe('Missed Shots', 0, '0');
+    await expectReviewFieldToBe('Quadrant', '3');
 
     // Validate TeleOp.
     await expectNthReviewFieldToBe('Upper Shots Made', 1, '0');
@@ -92,31 +164,37 @@
     await expectReviewFieldToBe('Battery died', 'false');
     await expectReviewFieldToBe('Broke (mechanically)', 'true');
 
-    // TODO(phil): Submit data and make sure it made its way to the database
-    // correctly. Right now the /requests/submit/data_scouting endpoint is not
-    // implemented.
+    await element(by.buttonText('Submit')).click();
+    await browser.wait(
+        EC.textToBePresentInElement(element(by.css('.header')), 'Success'));
+
+    // TODO(phil): Make sure the data made its way to the database correctly.
   });
 
   it('should: load all images successfully.', async () => {
     await loadPage();
 
+    await element(by.cssContainingText('.nav-link', 'Data Entry')).click();
+
     // Get to the Auto display with the field pictures.
     expect(await getHeadingText()).toEqual('Team Selection');
     await element(by.buttonText('Next')).click();
     expect(await getHeadingText()).toEqual('Auto');
 
     // We expect 2 fully loaded images.
-    browser.executeAsyncScript(function (callback) {
-      let images = document.getElementsByTagName('img');
-      let numLoaded = 0;
-      for (let i = 0; i < images.length; i += 1) {
-        if (images[i].naturalWidth > 0) {
-          numLoaded += 1;
-        }
-      }
-      callback(numLoaded);
-    }).then(function (numLoaded) {
-      expect(numLoaded).toBe(2);
-    });
+    browser
+        .executeAsyncScript(function(callback) {
+          let images = document.getElementsByTagName('img');
+          let numLoaded = 0;
+          for (let i = 0; i < images.length; i += 1) {
+            if (images[i].naturalWidth > 0) {
+              numLoaded += 1;
+            }
+          }
+          callback(numLoaded);
+        })
+        .then(function(numLoaded) {
+          expect(numLoaded).toBe(2);
+        });
   });
 });
diff --git a/scouting/webserver/requests/debug/cli/cli_test.py b/scouting/webserver/requests/debug/cli/cli_test.py
index b20703b..8bebc69 100644
--- a/scouting/webserver/requests/debug/cli/cli_test.py
+++ b/scouting/webserver/requests/debug/cli/cli_test.py
@@ -62,6 +62,12 @@
         json_path = write_json_request({
             "team": 100,
             "match": 1,
+            "starting_quadrant": 3,
+            "auto_ball_1": True,
+            "auto_ball_2": False,
+            "auto_ball_3": False,
+            "auto_ball_4": False,
+            "auto_ball_5": True,
             "missed_shots_auto": 10,
             "upper_goal_auto": 11,
             "lower_goal_auto": 12,
@@ -92,7 +98,13 @@
             LowerGoalTele: (int32) 15,
             DefenseRating: (int32) 3,
             Climbing: (int32) 1,
-            CollectedBy: (string) (len=9) "debug_cli"
+            CollectedBy: (string) (len=9) "debug_cli",
+            AutoBall1: (bool) true,
+            AutoBall2: (bool) false,
+            AutoBall3: (bool) false,
+            AutoBall4: (bool) false,
+            AutoBall5: (bool) true,
+            StartingQuadrant: (int32) 3
             }"""), stdout)
 
     def test_request_all_matches(self):
diff --git a/scouting/webserver/requests/messages/request_data_scouting_response.fbs b/scouting/webserver/requests/messages/request_data_scouting_response.fbs
index 5703a07..7d91cb7 100644
--- a/scouting/webserver/requests/messages/request_data_scouting_response.fbs
+++ b/scouting/webserver/requests/messages/request_data_scouting_response.fbs
@@ -3,6 +3,7 @@
 table Stats {
     team:int (id: 0);
     match:int (id: 1);
+
     missed_shots_auto:int (id: 2);
     upper_goal_auto:int (id:3);
     lower_goal_auto:int (id:4);
@@ -12,6 +13,15 @@
     defense_rating:int (id:8);
     climbing:int (id:9);
     collected_by:string (id:10);
+
+    auto_ball_1:bool (id:11);
+    auto_ball_2:bool (id:12);
+    auto_ball_3:bool (id:13);
+    auto_ball_4:bool (id:14);
+    auto_ball_5:bool (id:15);
+    // The quadrant that the robot starts in for autonomous mode.
+    // Valid values are 1 through 4.
+    starting_quadrant: int (id:16);
 }
 
 table RequestDataScoutingResponse {
diff --git a/scouting/webserver/requests/messages/submit_data_scouting.fbs b/scouting/webserver/requests/messages/submit_data_scouting.fbs
index 755e70b..d3d87e2 100644
--- a/scouting/webserver/requests/messages/submit_data_scouting.fbs
+++ b/scouting/webserver/requests/messages/submit_data_scouting.fbs
@@ -21,6 +21,14 @@
     // The rating that this robot gets for its climbing.
     // TODO: Change into an enum to make the different values self-documenting.
     climbing:int (id:9);
+    auto_ball_1:bool (id:11);
+    auto_ball_2:bool (id:12);
+    auto_ball_3:bool (id:13);
+    auto_ball_4:bool (id:14);
+    auto_ball_5:bool (id:15);
+    // The quadrant that the robot starts in for autonomous mode.
+    // Valid values are 1 through 4.
+    starting_quadrant: int (id:16);
 }
 
 root_type SubmitDataScouting;
diff --git a/scouting/webserver/requests/requests.go b/scouting/webserver/requests/requests.go
index 5a469b5..13d7396 100644
--- a/scouting/webserver/requests/requests.go
+++ b/scouting/webserver/requests/requests.go
@@ -142,8 +142,13 @@
 	}
 
 	stats := db.Stats{
-		TeamNumber:      request.Team(),
-		MatchNumber:     request.Match(),
+		TeamNumber:       request.Team(),
+		MatchNumber:      request.Match(),
+		StartingQuadrant: request.StartingQuadrant(),
+		AutoBallPickedUp: [5]bool{
+			request.AutoBall1(), request.AutoBall2(), request.AutoBall3(),
+			request.AutoBall4(), request.AutoBall5(),
+		},
 		ShotsMissedAuto: request.MissedShotsAuto(),
 		UpperGoalAuto:   request.UpperGoalAuto(),
 		LowerGoalAuto:   request.LowerGoalAuto(),
@@ -155,9 +160,17 @@
 		CollectedBy:     username,
 	}
 
+	// Do some error checking.
+	if stats.StartingQuadrant < 1 || stats.StartingQuadrant > 4 {
+		respondWithError(w, http.StatusBadRequest, fmt.Sprint(
+			"Invalid starting_quadrant field value of ", stats.StartingQuadrant))
+		return
+	}
+
 	err = handler.db.AddToStats(stats)
 	if err != nil {
 		respondWithError(w, http.StatusInternalServerError, fmt.Sprint("Failed to submit datascouting data: ", err))
+		return
 	}
 
 	builder := flatbuffers.NewBuilder(50 * 1024)
@@ -316,17 +329,23 @@
 	var response RequestDataScoutingResponseT
 	for _, stat := range stats {
 		response.StatsList = append(response.StatsList, &request_data_scouting_response.StatsT{
-			Team:            stat.TeamNumber,
-			Match:           stat.MatchNumber,
-			MissedShotsAuto: stat.ShotsMissedAuto,
-			UpperGoalAuto:   stat.UpperGoalAuto,
-			LowerGoalAuto:   stat.LowerGoalAuto,
-			MissedShotsTele: stat.ShotsMissed,
-			UpperGoalTele:   stat.UpperGoalShots,
-			LowerGoalTele:   stat.LowerGoalShots,
-			DefenseRating:   stat.PlayedDefense,
-			Climbing:        stat.Climbing,
-			CollectedBy:     stat.CollectedBy,
+			Team:             stat.TeamNumber,
+			Match:            stat.MatchNumber,
+			StartingQuadrant: stat.StartingQuadrant,
+			AutoBall1:        stat.AutoBallPickedUp[0],
+			AutoBall2:        stat.AutoBallPickedUp[1],
+			AutoBall3:        stat.AutoBallPickedUp[2],
+			AutoBall4:        stat.AutoBallPickedUp[3],
+			AutoBall5:        stat.AutoBallPickedUp[4],
+			MissedShotsAuto:  stat.ShotsMissedAuto,
+			UpperGoalAuto:    stat.UpperGoalAuto,
+			LowerGoalAuto:    stat.LowerGoalAuto,
+			MissedShotsTele:  stat.ShotsMissed,
+			UpperGoalTele:    stat.UpperGoalShots,
+			LowerGoalTele:    stat.LowerGoalShots,
+			DefenseRating:    stat.PlayedDefense,
+			Climbing:         stat.Climbing,
+			CollectedBy:      stat.CollectedBy,
 		})
 	}
 
diff --git a/scouting/webserver/requests/requests_test.go b/scouting/webserver/requests/requests_test.go
index c716b44..12d918b 100644
--- a/scouting/webserver/requests/requests_test.go
+++ b/scouting/webserver/requests/requests_test.go
@@ -82,16 +82,22 @@
 
 	builder := flatbuffers.NewBuilder(1024)
 	builder.Finish((&submit_data_scouting.SubmitDataScoutingT{
-		Team:            971,
-		Match:           1,
-		MissedShotsAuto: 9971,
-		UpperGoalAuto:   9971,
-		LowerGoalAuto:   9971,
-		MissedShotsTele: 9971,
-		UpperGoalTele:   9971,
-		LowerGoalTele:   9971,
-		DefenseRating:   9971,
-		Climbing:        9971,
+		Team:             971,
+		Match:            1,
+		StartingQuadrant: 2,
+		AutoBall1:        true,
+		AutoBall2:        false,
+		AutoBall3:        false,
+		AutoBall4:        false,
+		AutoBall5:        false,
+		MissedShotsAuto:  9971,
+		UpperGoalAuto:    9971,
+		LowerGoalAuto:    9971,
+		MissedShotsTele:  9971,
+		UpperGoalTele:    9971,
+		LowerGoalTele:    9971,
+		DefenseRating:    9971,
+		Climbing:         9971,
 	}).Pack(builder))
 
 	response, err := debug.SubmitDataScouting("http://localhost:8080", builder.FinishedBytes())
@@ -221,14 +227,18 @@
 		stats: []db.Stats{
 			{
 				TeamNumber: 971, MatchNumber: 1,
-				ShotsMissed: 1, UpperGoalShots: 2, LowerGoalShots: 3,
+				StartingQuadrant: 1,
+				AutoBallPickedUp: [5]bool{true, false, false, false, true},
+				ShotsMissed:      1, UpperGoalShots: 2, LowerGoalShots: 3,
 				ShotsMissedAuto: 4, UpperGoalAuto: 5, LowerGoalAuto: 6,
 				PlayedDefense: 7, Climbing: 8,
 				CollectedBy: "john",
 			},
 			{
 				TeamNumber: 972, MatchNumber: 1,
-				ShotsMissed: 2, UpperGoalShots: 3, LowerGoalShots: 4,
+				StartingQuadrant: 2,
+				AutoBallPickedUp: [5]bool{false, false, true, false, false},
+				ShotsMissed:      2, UpperGoalShots: 3, LowerGoalShots: 4,
 				ShotsMissedAuto: 5, UpperGoalAuto: 6, LowerGoalAuto: 7,
 				PlayedDefense: 8, Climbing: 9,
 				CollectedBy: "andrea",
@@ -255,12 +265,18 @@
 			// MissedShotsTele, UpperGoalTele, LowerGoalTele,
 			// DefenseRating, Climbing,
 			// CollectedBy,
+			// AutoBall1, AutoBall2, AutoBall3,
+			// AutoBall4, AutoBall5,
+			// StartingQuadrant,
 			{
 				971, 1,
 				4, 5, 6,
 				1, 2, 3,
 				7, 8,
 				"john",
+				true, false, false,
+				false, true,
+				1,
 			},
 			{
 				972, 1,
@@ -268,6 +284,9 @@
 				2, 3, 4,
 				8, 9,
 				"andrea",
+				false, false, true,
+				false, false,
+				2,
 			},
 		},
 	}
diff --git a/scouting/www/BUILD b/scouting/www/BUILD
index f0b91f6..f386f06 100644
--- a/scouting/www/BUILD
+++ b/scouting/www/BUILD
@@ -19,6 +19,7 @@
     deps = [
         "//scouting/www/entry",
         "//scouting/www/import_match_list",
+        "//scouting/www/match_list",
         "@npm//@angular/animations",
         "@npm//@angular/common",
         "@npm//@angular/core",
diff --git a/scouting/www/app.ng.html b/scouting/www/app.ng.html
index 2fafceb..fc21164 100644
--- a/scouting/www/app.ng.html
+++ b/scouting/www/app.ng.html
@@ -1,13 +1,22 @@
+<!-- Hidden element for protractor to disable alerts. -->
+<form class="visually-hidden">
+  <input type="checkbox" name="block_alerts" value="1" id="block_alerts" #block_alerts>
+</form>
+
 <ul class="nav nav-tabs">
   <li class="nav-item">
-    <a class="nav-link" [class.active]="tabIs('Entry')" (click)="switchTabTo('Entry')">Data Entry</a>
+    <a class="nav-link" [class.active]="tabIs('MatchList')" (click)="switchTabToGuarded('MatchList')">Match List</a>
   </li>
   <li class="nav-item">
-    <a class="nav-link" [class.active]="tabIs('ImportMatchList')" (click)="switchTabTo('ImportMatchList')">Import Match List</a>
+    <a class="nav-link" [class.active]="tabIs('Entry')" (click)="switchTabToGuarded('Entry')">Data Entry</a>
+  </li>
+  <li class="nav-item">
+    <a class="nav-link" [class.active]="tabIs('ImportMatchList')" (click)="switchTabToGuarded('ImportMatchList')">Import Match List</a>
   </li>
 </ul>
 
 <ng-container [ngSwitch]="tab">
-  <app-entry *ngSwitchCase="'Entry'"></app-entry>
+  <app-match-list (selectedTeamEvent)="selectTeamInMatch($event)" *ngSwitchCase="'MatchList'"></app-match-list>
+  <app-entry (switchTabsEvent)="switchTabTo($event)" [teamNumber]="selectedTeamInMatch.teamNumber" [matchNumber]="selectedTeamInMatch.matchNumber" *ngSwitchCase="'Entry'"></app-entry>
   <app-import-match-list *ngSwitchCase="'ImportMatchList'"></app-import-match-list>
 </ng-container>
diff --git a/scouting/www/app.ts b/scouting/www/app.ts
index 79c1094..fbde74f 100644
--- a/scouting/www/app.ts
+++ b/scouting/www/app.ts
@@ -1,6 +1,11 @@
-import {Component} from '@angular/core';
+import {Component, ElementRef, ViewChild} from '@angular/core';
 
-type Tab = 'Entry'|'ImportMatchList';
+type Tab = 'MatchList'|'Entry'|'ImportMatchList';
+type TeamInMatch = {
+  teamNumber: number,
+  matchNumber: number,
+  compLevel: string
+};
 
 @Component({
   selector: 'my-app',
@@ -8,14 +13,21 @@
   styleUrls: ['./common.css']
 })
 export class App {
-  tab: Tab = 'Entry';
+  selectedTeamInMatch:
+      TeamInMatch = {teamNumber: 1, matchNumber: 1, compLevel: 'qm'};
+  tab: Tab = 'MatchList';
+
+  @ViewChild('block_alerts') block_alerts: ElementRef;
 
   constructor() {
     window.addEventListener('beforeunload', (e) => {
-      // Based on https://developer.mozilla.org/en-US/docs/Web/API/WindowEventHandlers/onbeforeunload#example
-      // This combination ensures a dialog will be shown on most browsers.
-      e.preventDefault();
-      e.returnValue = '';
+      if (!this.block_alerts.nativeElement.checked) {
+        // Based on
+        // https://developer.mozilla.org/en-US/docs/Web/API/WindowEventHandlers/onbeforeunload#example
+        // This combination ensures a dialog will be shown on most browsers.
+        e.preventDefault();
+        e.returnValue = '';
+      }
     });
   }
 
@@ -23,13 +35,24 @@
     return this.tab == tab;
   }
 
-  switchTabTo(tab: Tab) {
+  selectTeamInMatch(teamInMatch: TeamInMatch) {
+    this.selectedTeamInMatch = teamInMatch;
+    this.switchTabTo('Entry');
+  }
+
+  switchTabToGuarded(tab: Tab) {
     let shouldSwitch = true;
-    if (tab === 'ImportMatchList') {
-      shouldSwitch = window.confirm('Leave data scouting page?');
+    if (this.tab !== tab) {
+      if (!this.block_alerts.nativeElement.checked) {
+        shouldSwitch = window.confirm('Leave current page?');
+      }
     }
     if (shouldSwitch) {
-      this.tab = tab;
+      this.switchTabTo(tab);
     }
   }
+
+  private switchTabTo(tab: Tab) {
+    this.tab = tab;
+  }
 }
diff --git a/scouting/www/app_module.ts b/scouting/www/app_module.ts
index 2a514f2..e560bfc 100644
--- a/scouting/www/app_module.ts
+++ b/scouting/www/app_module.ts
@@ -3,6 +3,7 @@
 import {BrowserAnimationsModule} from '@angular/platform-browser/animations';
 import {EntryModule} from './entry/entry.module';
 import {ImportMatchListModule} from './import_match_list/import_match_list.module';
+import {MatchListModule} from './match_list/match_list.module';
 
 import {App} from './app';
 
@@ -13,6 +14,7 @@
     BrowserAnimationsModule,
     EntryModule,
     ImportMatchListModule,
+    MatchListModule,
   ],
   exports: [App],
   bootstrap: [App],
diff --git a/scouting/www/common.css b/scouting/www/common.css
index 35e943f..43f117e 100644
--- a/scouting/www/common.css
+++ b/scouting/www/common.css
@@ -8,3 +8,10 @@
    * I.e. It would be nice to keep the error message and the progress message
    * aligned when they are non-empty. */
 }
+
+/* Hidden from the user, but interactable from protractor. */
+.visually-hidden {
+  position: absolute !important;
+  top: -9999px !important;
+  left: -9999px !important;
+}
diff --git a/scouting/www/entry/entry.component.ts b/scouting/www/entry/entry.component.ts
index aaf23dd..d35818d 100644
--- a/scouting/www/entry/entry.component.ts
+++ b/scouting/www/entry/entry.component.ts
@@ -1,119 +1,142 @@
-import { Component, ElementRef, OnInit, ViewChild } from '@angular/core';
-import { FormsModule } from '@angular/forms';
-
+import {Component, ElementRef, EventEmitter, Input, OnInit, Output, ViewChild} from '@angular/core';
+import {FormsModule} from '@angular/forms';
 import * as flatbuffer_builder from 'org_frc971/external/com_github_google_flatbuffers/ts/builder';
 import {ByteBuffer} from 'org_frc971/external/com_github_google_flatbuffers/ts/byte-buffer';
 import * as error_response from 'org_frc971/scouting/webserver/requests/messages/error_response_generated';
-import * as submit_data_scouting_response from 'org_frc971/scouting/webserver/requests/messages/submit_data_scouting_response_generated';
 import * as submit_data_scouting from 'org_frc971/scouting/webserver/requests/messages/submit_data_scouting_generated';
+import * as submit_data_scouting_response from 'org_frc971/scouting/webserver/requests/messages/submit_data_scouting_response_generated';
+
 import SubmitDataScouting = submit_data_scouting.scouting.webserver.requests.SubmitDataScouting;
 import SubmitDataScoutingResponse = submit_data_scouting_response.scouting.webserver.requests.SubmitDataScoutingResponse;
 import ErrorResponse = error_response.scouting.webserver.requests.ErrorResponse;
 
-type Section = 'Team Selection'|'Auto'|'TeleOp'|'Climb'|'Other'|'Review and Submit'|'Home'
-type Level = 'NoAttempt'|'Failed'|'FailedWithPlentyOfTime'|'Low'|'Medium'|'High'|'Transversal'
+type Section = 'Team Selection'|'Auto'|'TeleOp'|'Climb'|'Other'|
+    'Review and Submit'|'Success';
+type Level = 'NoAttempt'|'Failed'|'FailedWithPlentyOfTime'|'Low'|'Medium'|
+    'High'|'Transversal';
 
 @Component({
-    selector: 'app-entry',
-    templateUrl: './entry.ng.html',
-    styleUrls: ['../common.css', './entry.component.css']
+  selector: 'app-entry',
+  templateUrl: './entry.ng.html',
+  styleUrls: ['../common.css', './entry.component.css']
 })
 export class EntryComponent {
-    section: Section = 'Team Selection';
-    matchNumber: number = 1
-    teamNumber: number = 1
-    autoUpperShotsMade: number = 0;
-    autoLowerShotsMade: number = 0;
-    autoShotsMissed: number = 0;
-    teleUpperShotsMade: number = 0;
-    teleLowerShotsMade: number = 0;
-    teleShotsMissed: number = 0;
-    defensePlayedOnScore: number = 0;
-    defensePlayedScore: number = 0;
-    level: Level = 'NoAttempt';
-    errorMessage: string = '';
-    noShow: boolean = false;
-    neverMoved: boolean = false;
-    batteryDied: boolean = false;
-    mechanicallyBroke: boolean = false;
-    lostComs: boolean = false;
+  section: Section = 'Team Selection';
+  @Output() switchTabsEvent = new EventEmitter<string>();
+  @Input() matchNumber: number = 1;
+  @Input() teamNumber: number = 1;
+  autoUpperShotsMade: number = 0;
+  autoLowerShotsMade: number = 0;
+  autoShotsMissed: number = 0;
+  teleUpperShotsMade: number = 0;
+  teleLowerShotsMade: number = 0;
+  teleShotsMissed: number = 0;
+  defensePlayedOnScore: number = 0;
+  defensePlayedScore: number = 0;
+  level: Level = 'NoAttempt';
+  ball1: boolean = false;
+  ball2: boolean = false;
+  ball3: boolean = false;
+  ball4: boolean = false;
+  ball5: boolean = false;
+  quadrant: number = 1;
+  errorMessage: string = '';
+  noShow: boolean = false;
+  neverMoved: boolean = false;
+  batteryDied: boolean = false;
+  mechanicallyBroke: boolean = false;
+  lostComs: boolean = false;
 
-    @ViewChild("header") header: ElementRef;
+  @ViewChild('header') header: ElementRef;
 
-    nextSection() {
-        if (this.section === 'Team Selection') {
-            this.section = 'Auto';
-        } else if (this.section === 'Auto') {
-            this.section = 'TeleOp';
-        } else if (this.section === 'TeleOp') {
-            this.section = 'Climb';
-        } else if (this.section === 'Climb') {
-            this.section = 'Other';
-        } else if (this.section === 'Other') {
-            this.section = 'Review and Submit';
-        } else if (this.section === 'Review and Submit') {
-            this.submitDataScouting();
-            return;
-        }
-        // Scroll back to the top so that we can be sure the user sees the
-        // entire next screen. Otherwise it's easy to overlook input fields.
-        this.scrollToTop();
+  nextSection() {
+    if (this.section === 'Team Selection') {
+      this.section = 'Auto';
+    } else if (this.section === 'Auto') {
+      this.section = 'TeleOp';
+    } else if (this.section === 'TeleOp') {
+      this.section = 'Climb';
+    } else if (this.section === 'Climb') {
+      this.section = 'Other';
+    } else if (this.section === 'Other') {
+      this.section = 'Review and Submit';
+    } else if (this.section === 'Review and Submit') {
+      this.submitDataScouting();
+      return;
+    } else if (this.section === 'Success') {
+      this.switchTabsEvent.emit('MatchList');
+      return;
     }
+    // Scroll back to the top so that we can be sure the user sees the
+    // entire next screen. Otherwise it's easy to overlook input fields.
+    this.scrollToTop();
+  }
 
-    prevSection() {
-      if (this.section === 'Auto') {
-        this.section = 'Team Selection';
-      } else if (this.section === 'TeleOp') {
-        this.section = 'Auto';
-      } else if (this.section === 'Climb') {
-        this.section = 'TeleOp';
-      } else if (this.section === 'Other') {
-        this.section = 'Climb';
-      } else if (this.section === 'Review and Submit') {
-        this.section = 'Other';
-      }
-      // Scroll back to the top so that we can be sure the user sees the
-      // entire previous screen. Otherwise it's easy to overlook input
-      // fields.
-      this.scrollToTop();
+  prevSection() {
+    if (this.section === 'Auto') {
+      this.section = 'Team Selection';
+    } else if (this.section === 'TeleOp') {
+      this.section = 'Auto';
+    } else if (this.section === 'Climb') {
+      this.section = 'TeleOp';
+    } else if (this.section === 'Other') {
+      this.section = 'Climb';
+    } else if (this.section === 'Review and Submit') {
+      this.section = 'Other';
     }
+    // Scroll back to the top so that we can be sure the user sees the
+    // entire previous screen. Otherwise it's easy to overlook input
+    // fields.
+    this.scrollToTop();
+  }
 
-    private scrollToTop() {
-        this.header.nativeElement.scrollIntoView();
+  private scrollToTop() {
+    this.header.nativeElement.scrollIntoView();
+  }
+
+  async submitDataScouting() {
+    this.errorMessage = '';
+
+    const builder =
+        new flatbuffer_builder.Builder() as unknown as flatbuffers.Builder;
+    SubmitDataScouting.startSubmitDataScouting(builder);
+    SubmitDataScouting.addTeam(builder, this.teamNumber);
+    SubmitDataScouting.addMatch(builder, this.matchNumber);
+    SubmitDataScouting.addMissedShotsAuto(builder, this.autoShotsMissed);
+    SubmitDataScouting.addUpperGoalAuto(builder, this.autoUpperShotsMade);
+    SubmitDataScouting.addLowerGoalAuto(builder, this.autoLowerShotsMade);
+    SubmitDataScouting.addMissedShotsTele(builder, this.teleShotsMissed);
+    SubmitDataScouting.addUpperGoalTele(builder, this.teleUpperShotsMade);
+    SubmitDataScouting.addLowerGoalTele(builder, this.teleLowerShotsMade);
+    SubmitDataScouting.addDefenseRating(builder, this.defensePlayedScore);
+    SubmitDataScouting.addAutoBall1(builder, this.ball1);
+    SubmitDataScouting.addAutoBall2(builder, this.ball2);
+    SubmitDataScouting.addAutoBall3(builder, this.ball3);
+    SubmitDataScouting.addAutoBall4(builder, this.ball4);
+    SubmitDataScouting.addAutoBall5(builder, this.ball5);
+    SubmitDataScouting.addStartingQuadrant(builder, this.quadrant);
+
+    // TODO(phil): Add support for defensePlayedOnScore.
+    // TODO(phil): Fix the Climbing score.
+    SubmitDataScouting.addClimbing(builder, 1);
+    builder.finish(SubmitDataScouting.endSubmitDataScouting(builder));
+
+    const buffer = builder.asUint8Array();
+    const res = await fetch(
+        '/requests/submit/data_scouting', {method: 'POST', body: buffer});
+
+    if (res.ok) {
+      // We successfully submitted the data. Report success.
+      this.section = 'Success';
+    } else {
+      const resBuffer = await res.arrayBuffer();
+      const fbBuffer = new ByteBuffer(new Uint8Array(resBuffer));
+      const parsedResponse = ErrorResponse.getRootAsErrorResponse(
+          fbBuffer as unknown as flatbuffers.ByteBuffer);
+
+      const errorMessage = parsedResponse.errorMessage();
+      this.errorMessage =
+          `Received ${res.status} ${res.statusText}: "${errorMessage}"`;
     }
-
-    async submitDataScouting() {
-        const builder = new flatbuffer_builder.Builder() as unknown as flatbuffers.Builder;
-        SubmitDataScouting.startSubmitDataScouting(builder);
-        SubmitDataScouting.addTeam(builder, this.teamNumber);
-        SubmitDataScouting.addMatch(builder, this.matchNumber);
-        SubmitDataScouting.addMissedShotsAuto(builder, this.autoShotsMissed);
-        SubmitDataScouting.addUpperGoalAuto(builder, this.autoUpperShotsMade);
-        SubmitDataScouting.addLowerGoalAuto(builder, this.autoLowerShotsMade);
-        SubmitDataScouting.addMissedShotsTele(builder, this.teleShotsMissed);
-        SubmitDataScouting.addUpperGoalTele(builder, this.teleUpperShotsMade);
-        SubmitDataScouting.addLowerGoalTele(builder, this.teleLowerShotsMade);
-        SubmitDataScouting.addDefenseRating(builder, this.defensePlayedScore);
-        // TODO(phil): Add support for defensePlayedOnScore.
-        // TODO(phil): Fix the Climbing score.
-        SubmitDataScouting.addClimbing(builder, 1);
-        builder.finish(SubmitDataScouting.endSubmitDataScouting(builder));
-
-        const buffer = builder.asUint8Array();
-        const res = await fetch(
-            '/requests/submit/data_scouting', {method: 'POST', body: buffer});
-
-        if (res.ok) {
-            // We successfully submitted the data. Go back to Home.
-            this.section = 'Home';
-        } else {
-            const resBuffer = await res.arrayBuffer();
-            const fbBuffer = new ByteBuffer(new Uint8Array(resBuffer));
-            const parsedResponse = ErrorResponse.getRootAsErrorResponse(
-                fbBuffer as unknown as flatbuffers.ByteBuffer);
-
-            const errorMessage = parsedResponse.errorMessage();
-            this.errorMessage = `Received ${res.status} ${res.statusText}: "${errorMessage}"`;
-        }
-    }
+  }
 }
diff --git a/scouting/www/entry/entry.ng.html b/scouting/www/entry/entry.ng.html
index a8feaec..5849f3b 100644
--- a/scouting/www/entry/entry.ng.html
+++ b/scouting/www/entry/entry.ng.html
@@ -21,27 +21,27 @@
 
     <div *ngSwitchCase="'Auto'" id="auto" class="container-fluid">
         <div class="row">
-            <img src="/pictures/field/balls.jpeg" alt="Image">
+            <img src="/pictures/field/quadrants.jpeg" alt="Quadrants Image">
             <form>
-                <!--Choice for each ball location-->
-                <input type="checkbox" name="balls" value="1" id="ball-1"><label for="ball-1">Ball 1</label>
-                <input type="checkbox" name="balls" value="2" id="ball-2"><label for="ball-2">Ball 2</label><br>
-                <input type="checkbox" name="balls" value="3" id="ball-3"><label for="ball-3">Ball 3</label>
-                <input type="checkbox" name="balls" value="4" id="ball-4"><label for="ball-4">Ball 4</label><br>
-                <input type="checkbox" name="balls" value="5" id="ball-5"><label for="ball-5">Ball 5</label>
+                <input type="radio" [(ngModel)]="quadrant" name="quadrant" id="quadrant1" [value]="1">
+                <label for="quadrant1">Quadrant 1</label>
+                <input type="radio" [(ngModel)]="quadrant" name="quadrant" id="quadrant2" [value]="2">
+                <label for="quadrant2">Quadrant 2</label><br>
+                <input type="radio" [(ngModel)]="quadrant" name="quadrant" id="quadrant3" [value]="3">
+                <label for="quadrant3">Quadrant 3</label>
+                <input type="radio" [(ngModel)]="quadrant" name="quadrant" id="quadrant4" [value]="4">
+                <label for="quadrant4">Quadrant 4</label>
             </form>
         </div>
         <div class="row">
-            <img src="/pictures/field/quadrants.jpeg" alt="Image">
+            <img src="/pictures/field/balls.jpeg" alt="Image">
             <form>
-                <input type="radio" name="quadrant" id="first" value="Quadrant 1">
-                <label for="first">Quadrant 1</label>
-                <input type="radio" name="quadrant" id="second" value="Quadrant 2">
-                <label for="second">Quadrant 2</label><br>
-                <input type="radio" name="quadrant" id="third" value="Quadrant 3">
-                <label for="third">Quadrant 3</label>
-                <input type="radio" name="quadrant" id="fourth" value="Quadrant 4">
-                <label for="fourth">Quadrant 4</label>
+                <!--Choice for each ball location-->
+                <input [(ngModel)]=ball1 type="checkbox" name="1ball" value="1" id="ball-1"><label for="ball-1">Ball 1</label>
+                <input [(ngModel)]=ball2 type="checkbox" name="2ball" value="2" id="ball-2"><label for="ball-2">Ball 2</label><br>
+                <input [(ngModel)]=ball3 type="checkbox" name="3ball" value="3" id="ball-3"><label for="ball-3">Ball 3</label>
+                <input [(ngModel)]=ball4 type="checkbox" name="4ball" value="4" id="ball-4"><label for="ball-4">Ball 4</label><br>
+                <input [(ngModel)]=ball5 type="checkbox" name="5ball" value="5" id="ball-5"><label for="ball-5">Ball 5</label>
             </form>
         </div>
         <div class="row justify-content-center">
@@ -157,6 +157,12 @@
 
         <h4>Auto</h4>
         <ul>
+            <li>Quadrant: {{quadrant}}</li>
+            <li>Collected Ball 1: {{ball1}}</li>
+            <li>Collected Ball 2: {{ball2}}</li>
+            <li>Collected Ball 3: {{ball3}}</li>
+            <li>Collected Ball 4: {{ball4}}</li>
+            <li>Collected Ball 5: {{ball5}}</li>
             <li>Upper Shots Made: {{autoUpperShotsMade}}</li>
             <li>Lower Shots Made: {{autoLowerShotsMade}}</li>
             <li>Missed Shots: {{autoShotsMissed}}</li>
@@ -187,9 +193,16 @@
 
         <span class="error_message">{{ errorMessage }}</span>
 
-        <div class="buttons">
-          <button class="btn btn-primary" (click)="prevSection()">Back</button>
+        <div class="buttons justify-content-end">
           <button class="btn btn-primary" (click)="nextSection()">Submit</button>
         </div>
     </div>
+
+    <div *ngSwitchCase="'Success'" id="success" class="container-fluid">
+        <span>Successfully submitted scouting data.</span>
+        <div class="buttons justify-content-end">
+          <button class="btn btn-primary" (click)="nextSection()">Continue</button>
+        </div>
+
+    </div>
 </ng-container>
diff --git a/scouting/www/import_match_list/import_match_list.component.ts b/scouting/www/import_match_list/import_match_list.component.ts
index 7ad4ab6..bb7e73e 100644
--- a/scouting/www/import_match_list/import_match_list.component.ts
+++ b/scouting/www/import_match_list/import_match_list.component.ts
@@ -21,8 +21,12 @@
   errorMessage: string = '';
 
   async importMatchList() {
-    if (!window.confirm('Actually import new matches?')) {
-      return;
+    const block_alerts = document.getElementById('block_alerts') as HTMLInputElement;
+    console.log(block_alerts.checked);
+    if (!block_alerts.checked) {
+      if (!window.confirm('Actually import new matches?')) {
+        return;
+      }
     }
 
     this.errorMessage = '';
diff --git a/scouting/www/index.html b/scouting/www/index.html
index 3a09dfd..b5c33e6 100644
--- a/scouting/www/index.html
+++ b/scouting/www/index.html
@@ -1,6 +1,7 @@
 <!DOCTYPE html>
 <html>
   <head>
+    <title>FRC971 Scouting Application</title>
     <meta name="viewport" content="width=device-width, initial-scale=1">
     <base href="/">
     <script src="./npm/node_modules/zone.js/dist/zone.min.js"></script>
diff --git a/scouting/www/match_list/BUILD b/scouting/www/match_list/BUILD
new file mode 100644
index 0000000..a33caf8
--- /dev/null
+++ b/scouting/www/match_list/BUILD
@@ -0,0 +1,27 @@
+load("@npm//@bazel/typescript:index.bzl", "ts_library")
+
+ts_library(
+    name = "match_list",
+    srcs = [
+        "match_list.component.ts",
+        "match_list.module.ts",
+    ],
+    angular_assets = [
+        "match_list.component.css",
+        "match_list.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:request_all_matches_response_ts_fbs",
+        "//scouting/webserver/requests/messages:request_all_matches_ts_fbs",
+        "@com_github_google_flatbuffers//ts:flatbuffers_ts",
+        "@npm//@angular/common",
+        "@npm//@angular/core",
+        "@npm//@angular/forms",
+    ],
+)
diff --git a/scouting/www/match_list/match_list.component.css b/scouting/www/match_list/match_list.component.css
new file mode 100644
index 0000000..a2c1676
--- /dev/null
+++ b/scouting/www/match_list/match_list.component.css
@@ -0,0 +1,11 @@
+* {
+    padding: 10px;
+}
+
+.red {
+  background-color: #dc3545;
+}
+
+.blue {
+  background-color: #0d6efd;
+}
diff --git a/scouting/www/match_list/match_list.component.ts b/scouting/www/match_list/match_list.component.ts
new file mode 100644
index 0000000..0085b7b
--- /dev/null
+++ b/scouting/www/match_list/match_list.component.ts
@@ -0,0 +1,122 @@
+import {Component, EventEmitter, OnInit, Output} from '@angular/core';
+import * as flatbuffer_builder from 'org_frc971/external/com_github_google_flatbuffers/ts/builder';
+import {ByteBuffer} from 'org_frc971/external/com_github_google_flatbuffers/ts/byte-buffer';
+import * as error_response from 'org_frc971/scouting/webserver/requests/messages/error_response_generated';
+import * as request_all_matches from 'org_frc971/scouting/webserver/requests/messages/request_all_matches_generated';
+import * as request_all_matches_response from 'org_frc971/scouting/webserver/requests/messages/request_all_matches_response_generated';
+
+import RequestAllMatches = request_all_matches.scouting.webserver.requests.RequestAllMatches;
+import RequestAllMatchesResponse = request_all_matches_response.scouting.webserver.requests.RequestAllMatchesResponse;
+import Match = request_all_matches_response.scouting.webserver.requests.Match;
+import ErrorResponse = error_response.scouting.webserver.requests.ErrorResponse;
+
+type TeamInMatch = {
+  teamNumber: number,
+  matchNumber: number,
+  compLevel: string
+};
+
+@Component({
+  selector: 'app-match-list',
+  templateUrl: './match_list.ng.html',
+  styleUrls: ['../common.css', './match_list.component.css']
+})
+export class MatchListComponent implements OnInit {
+  @Output() selectedTeamEvent = new EventEmitter<TeamInMatch>();
+  teamInMatch: TeamInMatch = {teamNumber: 1, matchNumber: 1, compLevel: 'qm'};
+  progressMessage: string = '';
+  errorMessage: string = '';
+  matchList: Match[] = [];
+
+  setTeamInMatch(teamInMatch: TeamInMatch) {
+    this.teamInMatch = teamInMatch;
+    this.selectedTeamEvent.emit(teamInMatch);
+  }
+
+  teamsInMatch(match: Match): {number: number, color: 'red'|'blue'}[] {
+    return [
+      {number: match.r1(), color: 'red'},
+      {number: match.r2(), color: 'red'},
+      {number: match.r3(), color: 'red'},
+      {number: match.b1(), color: 'blue'},
+      {number: match.b2(), color: 'blue'},
+      {number: match.b3(), color: 'blue'},
+    ];
+  }
+
+  matchType(match: Match): string|null {
+    switch (match.compLevel()) {
+      case 'qm':
+        return 'Quals';
+      case 'ef':
+        return 'Eighth Final';
+      case 'qf':
+        return 'Quarter Final';
+      case 'sf':
+        return 'Semi Final';
+      case 'f':
+        return 'Final';
+      default:
+        return null;
+    }
+  }
+
+  displayMatchNumber(match: Match): string {
+    return `${this.matchType(match)} ${match.matchNumber()}`;
+  }
+
+  ngOnInit() {
+    this.importMatchList();
+  }
+
+  async importMatchList() {
+    this.errorMessage = '';
+
+    const builder =
+        new flatbuffer_builder.Builder() as unknown as flatbuffers.Builder;
+    RequestAllMatches.startRequestAllMatches(builder);
+    builder.finish(RequestAllMatches.endRequestAllMatches(builder));
+
+    this.progressMessage = 'Fetching match list. Please be patient.';
+
+    const buffer = builder.asUint8Array();
+    const res = await fetch(
+        '/requests/request/all_matches', {method: 'POST', body: buffer});
+
+    if (res.ok) {
+      const resBuffer = await res.arrayBuffer();
+      const fbBuffer = new ByteBuffer(new Uint8Array(resBuffer));
+      const parsedResponse =
+          RequestAllMatchesResponse.getRootAsRequestAllMatchesResponse(
+              fbBuffer as unknown as flatbuffers.ByteBuffer);
+
+      this.matchList = [];
+      for (let i = 0; i < parsedResponse.matchListLength(); i++) {
+        this.matchList.push(parsedResponse.matchList(i));
+      }
+      this.matchList.sort((a, b) => {
+        let aString = this.displayMatchNumber(a);
+        let bString = this.displayMatchNumber(b);
+        if (aString < bString) {
+          return -1;
+        }
+        if (aString > bString) {
+          return 1;
+        }
+        return 0;
+      });
+
+      this.progressMessage = 'Successfully fetched match list.';
+    } else {
+      this.progressMessage = '';
+      const resBuffer = await res.arrayBuffer();
+      const fbBuffer = new ByteBuffer(new Uint8Array(resBuffer));
+      const parsedResponse = ErrorResponse.getRootAsErrorResponse(
+          fbBuffer as unknown as flatbuffers.ByteBuffer);
+
+      const errorMessage = parsedResponse.errorMessage();
+      this.errorMessage =
+          `Received ${res.status} ${res.statusText}: "${errorMessage}"`;
+    }
+  }
+}
diff --git a/scouting/www/match_list/match_list.module.ts b/scouting/www/match_list/match_list.module.ts
new file mode 100644
index 0000000..dbec410
--- /dev/null
+++ b/scouting/www/match_list/match_list.module.ts
@@ -0,0 +1,13 @@
+import {CommonModule} from '@angular/common';
+import {NgModule} from '@angular/core';
+import {FormsModule} from '@angular/forms';
+
+import {MatchListComponent} from './match_list.component';
+
+@NgModule({
+  declarations: [MatchListComponent],
+  exports: [MatchListComponent],
+  imports: [CommonModule, FormsModule],
+})
+export class MatchListModule {
+}
diff --git a/scouting/www/match_list/match_list.ng.html b/scouting/www/match_list/match_list.ng.html
new file mode 100644
index 0000000..39d4578
--- /dev/null
+++ b/scouting/www/match_list/match_list.ng.html
@@ -0,0 +1,30 @@
+<div class="header">
+    <h2>Matches</h2>
+</div>
+
+<div class="container-fluid">
+
+  <div class="row">
+
+    <div *ngFor="let match of matchList; index as i">
+      <span class="badge bg-secondary rounded-left">{{ displayMatchNumber(match) }}</span>
+      <div class="list-group list-group-horizontal-sm">
+        <button
+          *ngFor="let team of teamsInMatch(match);"
+          (click)="setTeamInMatch({
+            teamNumber: team.number,
+            matchNumber: match.matchNumber(),
+            compLevel: match.compLevel()
+          })"
+          class="text-center text-white fw-bold
+            list-group-item list-group-item-action"
+          [ngClass]="team.color">
+          {{ team.number }}
+        </button>
+      </div>
+    </div>
+  </div>
+
+  <span class="progress_message" role="alert">{{ progressMessage }}</span>
+  <span class="error_message" role="alert">{{ errorMessage }}</span>
+</div>
diff --git a/third_party/y2022/field/balls.jpeg b/third_party/y2022/field/balls.jpeg
index 9c7cc7f..8e0fba3 100644
--- a/third_party/y2022/field/balls.jpeg
+++ b/third_party/y2022/field/balls.jpeg
Binary files differ