Add delete functionality to the scouting app

This patch adds the ability to delete data scouting entries
from the database through the view tab.

Signed-off-by: Filip Kujawa <filip.j.kujawa@gmail.com>
Change-Id: I294fd3ebfa3721dace00582ba7f22e3da5f0f419
diff --git a/BUILD b/BUILD
index d2c1658..5427fd5 100644
--- a/BUILD
+++ b/BUILD
@@ -69,6 +69,8 @@
 # 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:resolve go github.com/frc971/971-Robot-Code/scouting/webserver/requests/messages/delete_2023_data_scouting //scouting/webserver/requests/messages:delete_2023_data_scouting_go_fbs
+# gazelle:resolve go github.com/frc971/971-Robot-Code/scouting/webserver/requests/messages/delete_2023_data_scouting_response //scouting/webserver/requests/messages:delete_2023_data_scouting_response_go_fbs
 
 gazelle(
     name = "gazelle",
diff --git a/scouting/db/db.go b/scouting/db/db.go
index ac9a6e8..1a43634 100644
--- a/scouting/db/db.go
+++ b/scouting/db/db.go
@@ -199,6 +199,14 @@
 	return result.Error
 }
 
+func (database *Database) DeleteFromActions(compLevel_ string, matchNumber_ int32, setNumber_ int32, teamNumber_ string) error {
+	var actions []Action
+	result := database.
+		Where("comp_level = ? AND match_number = ? AND set_number = ? AND team_number = ?", compLevel_, matchNumber_, setNumber_, teamNumber_).
+		Delete(&actions)
+	return result.Error
+}
+
 func (database *Database) AddOrUpdateRankings(r Ranking) error {
 	result := database.Clauses(clause.OnConflict{
 		UpdateAll: true,
diff --git a/scouting/db/db_test.go b/scouting/db/db_test.go
index 8a4c0bc..d49e649 100644
--- a/scouting/db/db_test.go
+++ b/scouting/db/db_test.go
@@ -505,6 +505,75 @@
 	}
 }
 
+func TestDeleteFromActions(t *testing.T) {
+	fixture := createDatabase(t)
+	defer fixture.TearDown()
+
+	startingActions := []Action{
+		Action{
+			TeamNumber: "1235", MatchNumber: 94, SetNumber: 1, CompLevel: "quals",
+			CompletedAction: []byte(""), Timestamp: 0000, CollectedBy: "",
+		},
+		Action{
+			TeamNumber: "1236", MatchNumber: 94, SetNumber: 1, CompLevel: "quals",
+			CompletedAction: []byte(""), Timestamp: 0321, CollectedBy: "",
+		},
+		Action{
+			TeamNumber: "1237", MatchNumber: 94, SetNumber: 1, CompLevel: "quals",
+			CompletedAction: []byte(""), Timestamp: 0222, CollectedBy: "",
+		},
+		Action{
+			TeamNumber: "1238", MatchNumber: 94, SetNumber: 1, CompLevel: "quals",
+			CompletedAction: []byte(""), Timestamp: 0110, CollectedBy: "",
+		},
+		Action{
+			TeamNumber: "1239", MatchNumber: 94, SetNumber: 1, CompLevel: "quals",
+			CompletedAction: []byte(""), Timestamp: 0004, CollectedBy: "",
+		},
+		Action{
+			TeamNumber: "1233", MatchNumber: 94, SetNumber: 1, CompLevel: "quals",
+			CompletedAction: []byte(""), Timestamp: 0005, CollectedBy: "",
+		},
+	}
+
+	correct := []Action{
+		Action{
+			TeamNumber: "1235", MatchNumber: 94, SetNumber: 1, CompLevel: "quals",
+			CompletedAction: []byte(""), Timestamp: 0000, CollectedBy: "",
+		},
+		Action{
+			TeamNumber: "1236", MatchNumber: 94, SetNumber: 1, CompLevel: "quals",
+			CompletedAction: []byte(""), Timestamp: 0321, CollectedBy: "",
+		},
+		Action{
+			TeamNumber: "1237", MatchNumber: 94, SetNumber: 1, CompLevel: "quals",
+			CompletedAction: []byte(""), Timestamp: 0222, CollectedBy: "",
+		},
+		Action{
+			TeamNumber: "1238", MatchNumber: 94, SetNumber: 1, CompLevel: "quals",
+			CompletedAction: []byte(""), Timestamp: 0110, CollectedBy: "",
+		},
+		Action{
+			TeamNumber: "1233", MatchNumber: 94, SetNumber: 1, CompLevel: "quals",
+			CompletedAction: []byte(""), Timestamp: 0005, CollectedBy: "",
+		},
+	}
+
+	for _, action := range startingActions {
+		err := fixture.db.AddAction(action)
+		check(t, err, "Failed to add stat")
+	}
+
+	err := fixture.db.DeleteFromActions("quals", 94, 1, "1239")
+
+	got, err := fixture.db.ReturnActions()
+	check(t, err, "Failed ReturnActions()")
+
+	if !reflect.DeepEqual(correct, got) {
+		t.Errorf("Got %#v,\nbut expected %#v.", got, correct)
+	}
+}
+
 func TestQueryShiftDB(t *testing.T) {
 	fixture := createDatabase(t)
 	defer fixture.TearDown()
diff --git a/scouting/scouting_test.cy.js b/scouting/scouting_test.cy.js
index 52d838b..b1276d9 100644
--- a/scouting/scouting_test.cy.js
+++ b/scouting/scouting_test.cy.js
@@ -8,6 +8,10 @@
 // (index 3) which resolves to team 3990 in quals match 1.
 const QUALS_MATCH_1_TEAM_3990 = 0 * 6 + 3;
 
+// On the 2st row of matches (index 1) click on the fourth team
+// (index 3) which resolves to team 4481 in quals match 1.
+const QUALS_MATCH_2_TEAM_4481 = 1 * 6 + 3;
+
 function disableAlerts() {
   cy.get('#block_alerts').check({force: true}).should('be.checked');
 }
@@ -66,6 +70,54 @@
   return element;
 }
 
+function submitDataScouting(
+  matchButtonKey = SEMI_FINAL_2_MATCH_3_TEAM_5254,
+  teamNumber = 5254
+) {
+  // Click on a random team in the Match list. The exact details here are not
+  // important, but we need to know what they are. This could as well be any
+  // other team from any other match.
+  cy.get('button.match-item').eq(matchButtonKey).click();
+
+  // Select Starting Position.
+  headerShouldBe(teamNumber + ' Init ');
+  cy.get('[type="radio"]').first().check();
+  clickButton('Start Match');
+
+  // Pick and Place Cone in Auto.
+  clickButton('CONE');
+  clickButton('HIGH');
+
+  // Pick and Place Cube in Teleop.
+  clickButton('Start Teleop');
+  clickButton('CUBE');
+  clickButton('LOW');
+
+  // Robot dead and revive.
+  clickButton('DEAD');
+  clickButton('Revive');
+
+  // Endgame.
+  clickButton('Endgame');
+  cy.contains(/Docked & Engaged/).click();
+
+  clickButton('End Match');
+  headerShouldBe(teamNumber + ' Review and Submit ');
+  cy.get('#review_data li')
+    .eq(0)
+    .should('have.text', ' Started match at position 1 ');
+  cy.get('#review_data li').eq(1).should('have.text', ' Picked up kCone ');
+  cy.get('#review_data li')
+    .last()
+    .should(
+      'have.text',
+      ' Ended Match; docked: false, engaged: true, attempted to dock and engage: false '
+    );
+
+  clickButton('Submit');
+  headerShouldBe(teamNumber + ' Success ');
+}
+
 before(() => {
   cy.visit('/');
   disableAlerts();
@@ -139,48 +191,7 @@
 
   //TODO(FILIP): Verify last action when the last action header gets added.
   it('should: be able to submit data scouting.', () => {
-    // Click on a random team in the Match list. The exact details here are not
-    // important, but we need to know what they are. This could as well be any
-    // other team from any other match.
-    cy.get('button.match-item').eq(SEMI_FINAL_2_MATCH_3_TEAM_5254).click();
-
-    // Select Starting Position.
-    headerShouldBe('5254 Init ');
-    cy.get('[type="radio"]').first().check();
-    clickButton('Start Match');
-
-    // Pick and Place Cone in Auto.
-    clickButton('CONE');
-    clickButton('HIGH');
-
-    // Pick and Place Cube in Teleop.
-    clickButton('Start Teleop');
-    clickButton('CUBE');
-    clickButton('LOW');
-
-    // Robot dead and revive.
-    clickButton('DEAD');
-    clickButton('Revive');
-
-    // Endgame.
-    clickButton('Endgame');
-    cy.contains(/Docked & Engaged/).click();
-
-    clickButton('End Match');
-    headerShouldBe('5254 Review and Submit ');
-    cy.get('#review_data li')
-      .eq(0)
-      .should('have.text', ' Started match at position 1 ');
-    cy.get('#review_data li').eq(1).should('have.text', ' Picked up kCone ');
-    cy.get('#review_data li')
-      .last()
-      .should(
-        'have.text',
-        ' Ended Match; docked: false, engaged: true, attempted to dock and engage: false '
-      );
-
-    clickButton('Submit');
-    headerShouldBe('5254 Success ');
+    submitDataScouting();
 
     // Now that the data is submitted, the button should be disabled.
     switchToTab('Match List');
@@ -189,6 +200,30 @@
       .should('be.disabled');
   });
 
+  it('should: be able to delete data scouting entry', () => {
+    // Submit data to delete.
+    submitDataScouting(QUALS_MATCH_2_TEAM_4481, 4481);
+
+    switchToTab('View');
+
+    cy.get('[data-bs-toggle="dropdown"]').click();
+    cy.get('[id="stats_source_dropdown"]').click();
+
+    // Check that table contains data.
+    cy.get('table.table tbody td').should('contain', '4481');
+
+    // Find and click the delete button for the row containing team 4481.
+    cy.get('table.table tbody td')
+      .contains('4481')
+      .parent()
+      .find('[id^="delete_button_"]')
+      .click();
+    cy.on('window:confirm', () => true);
+
+    // Check that deleted data is not in table.
+    cy.get('table.table tbody').should('not.contain', '4481');
+  });
+
   it('should: be able to return to correct screen with undo for pick and place.', () => {
     cy.get('button.match-item').eq(QUALS_MATCH_1_TEAM_3990).click();
 
diff --git a/scouting/webserver/requests/BUILD b/scouting/webserver/requests/BUILD
index 2ff8b85..795d0ee 100644
--- a/scouting/webserver/requests/BUILD
+++ b/scouting/webserver/requests/BUILD
@@ -8,6 +8,8 @@
     visibility = ["//visibility:public"],
     deps = [
         "//scouting/db",
+        "//scouting/webserver/requests/messages:delete_2023_data_scouting_go_fbs",
+        "//scouting/webserver/requests/messages:delete_2023_data_scouting_response_go_fbs",
         "//scouting/webserver/requests/messages:error_response_go_fbs",
         "//scouting/webserver/requests/messages:request_2023_data_scouting_go_fbs",
         "//scouting/webserver/requests/messages:request_2023_data_scouting_response_go_fbs",
@@ -42,6 +44,7 @@
     deps = [
         "//scouting/db",
         "//scouting/webserver/requests/debug",
+        "//scouting/webserver/requests/messages:delete_2023_data_scouting_go_fbs",
         "//scouting/webserver/requests/messages:request_2023_data_scouting_go_fbs",
         "//scouting/webserver/requests/messages:request_2023_data_scouting_response_go_fbs",
         "//scouting/webserver/requests/messages:request_all_driver_rankings_go_fbs",
diff --git a/scouting/webserver/requests/debug/BUILD b/scouting/webserver/requests/debug/BUILD
index d9bb030..ef14e5a 100644
--- a/scouting/webserver/requests/debug/BUILD
+++ b/scouting/webserver/requests/debug/BUILD
@@ -7,6 +7,7 @@
     target_compatible_with = ["@platforms//cpu:x86_64"],
     visibility = ["//visibility:public"],
     deps = [
+        "//scouting/webserver/requests/messages:delete_2023_data_scouting_response_go_fbs",
         "//scouting/webserver/requests/messages:error_response_go_fbs",
         "//scouting/webserver/requests/messages:request_2023_data_scouting_response_go_fbs",
         "//scouting/webserver/requests/messages:request_all_driver_rankings_response_go_fbs",
diff --git a/scouting/webserver/requests/debug/debug.go b/scouting/webserver/requests/debug/debug.go
index acb9dd4..4837cfe 100644
--- a/scouting/webserver/requests/debug/debug.go
+++ b/scouting/webserver/requests/debug/debug.go
@@ -9,6 +9,7 @@
 	"log"
 	"net/http"
 
+	"github.com/frc971/971-Robot-Code/scouting/webserver/requests/messages/delete_2023_data_scouting_response"
 	"github.com/frc971/971-Robot-Code/scouting/webserver/requests/messages/error_response"
 	"github.com/frc971/971-Robot-Code/scouting/webserver/requests/messages/request_2023_data_scouting_response"
 	"github.com/frc971/971-Robot-Code/scouting/webserver/requests/messages/request_all_driver_rankings_response"
@@ -164,3 +165,9 @@
 		server+"/requests/submit/submit_actions", requestBytes,
 		submit_actions_response.GetRootAsSubmitActionsResponse)
 }
+
+func Delete2023DataScouting(server string, requestBytes []byte) (*delete_2023_data_scouting_response.Delete2023DataScoutingResponseT, error) {
+	return sendMessage[delete_2023_data_scouting_response.Delete2023DataScoutingResponseT](
+		server+"/requests/delete/delete_2023_data_scouting", requestBytes,
+		delete_2023_data_scouting_response.GetRootAsDelete2023DataScoutingResponse)
+}
diff --git a/scouting/webserver/requests/messages/BUILD b/scouting/webserver/requests/messages/BUILD
index c1cd999..db422ed 100644
--- a/scouting/webserver/requests/messages/BUILD
+++ b/scouting/webserver/requests/messages/BUILD
@@ -23,6 +23,8 @@
     "submit_driver_ranking_response",
     "submit_actions",
     "submit_actions_response",
+    "delete_2023_data_scouting",
+    "delete_2023_data_scouting_response",
 )
 
 filegroup(
diff --git a/scouting/webserver/requests/messages/delete_2023_data_scouting.fbs b/scouting/webserver/requests/messages/delete_2023_data_scouting.fbs
new file mode 100644
index 0000000..a2a3ce6
--- /dev/null
+++ b/scouting/webserver/requests/messages/delete_2023_data_scouting.fbs
@@ -0,0 +1,10 @@
+namespace scouting.webserver.requests;
+
+table Delete2023DataScouting {
+    comp_level:string (id: 0);
+    match_number:int (id: 1);
+    set_number:int (id: 2);
+    team_number:string (id: 3);
+}
+
+root_type Delete2023DataScouting;
diff --git a/scouting/webserver/requests/messages/delete_2023_data_scouting_response.fbs b/scouting/webserver/requests/messages/delete_2023_data_scouting_response.fbs
new file mode 100644
index 0000000..fd07526
--- /dev/null
+++ b/scouting/webserver/requests/messages/delete_2023_data_scouting_response.fbs
@@ -0,0 +1,7 @@
+namespace scouting.webserver.requests;
+
+table Delete2023DataScoutingResponse {
+    // empty response
+}
+
+root_type Delete2023DataScoutingResponse;
diff --git a/scouting/webserver/requests/requests.go b/scouting/webserver/requests/requests.go
index 37a6ff2..533959c 100644
--- a/scouting/webserver/requests/requests.go
+++ b/scouting/webserver/requests/requests.go
@@ -12,6 +12,8 @@
 	"strings"
 
 	"github.com/frc971/971-Robot-Code/scouting/db"
+	"github.com/frc971/971-Robot-Code/scouting/webserver/requests/messages/delete_2023_data_scouting"
+	"github.com/frc971/971-Robot-Code/scouting/webserver/requests/messages/delete_2023_data_scouting_response"
 	"github.com/frc971/971-Robot-Code/scouting/webserver/requests/messages/error_response"
 	"github.com/frc971/971-Robot-Code/scouting/webserver/requests/messages/request_2023_data_scouting"
 	"github.com/frc971/971-Robot-Code/scouting/webserver/requests/messages/request_2023_data_scouting_response"
@@ -58,6 +60,8 @@
 type SubmitActions = submit_actions.SubmitActions
 type Action = submit_actions.Action
 type SubmitActionsResponseT = submit_actions_response.SubmitActionsResponseT
+type Delete2023DataScouting = delete_2023_data_scouting.Delete2023DataScouting
+type Delete2023DataScoutingResponseT = delete_2023_data_scouting_response.Delete2023DataScoutingResponseT
 
 // The interface we expect the database abstraction to conform to.
 // We use an interface here because it makes unit testing easier.
@@ -76,6 +80,8 @@
 	AddNotes(db.NotesData) error
 	AddDriverRanking(db.DriverRankingData) error
 	AddAction(db.Action) error
+	DeleteFromStats(string, int32, int32, string) error
+	DeleteFromActions(string, int32, int32, string) error
 }
 
 // Handles unknown requests. Just returns a 404.
@@ -871,6 +877,50 @@
 	w.Write(builder.FinishedBytes())
 }
 
+type Delete2023DataScoutingHandler struct {
+	db Database
+}
+
+func (handler Delete2023DataScoutingHandler) 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, "Delete2023DataScouting", delete_2023_data_scouting.GetRootAsDelete2023DataScouting)
+	if !success {
+		return
+	}
+
+	err = handler.db.DeleteFromStats(
+		string(request.CompLevel()),
+		request.MatchNumber(),
+		request.SetNumber(),
+		string(request.TeamNumber()))
+
+	if err != nil {
+		respondWithError(w, http.StatusInternalServerError, fmt.Sprintf("Failed to delete from stats: %v", err))
+		return
+	}
+
+	err = handler.db.DeleteFromActions(
+		string(request.CompLevel()),
+		request.MatchNumber(),
+		request.SetNumber(),
+		string(request.TeamNumber()))
+
+	if err != nil {
+		respondWithError(w, http.StatusInternalServerError, fmt.Sprintf("Failed to delete from actions: %v", err))
+		return
+	}
+
+	var response Delete2023DataScoutingResponseT
+	builder := flatbuffers.NewBuilder(10)
+	builder.Finish((&response).Pack(builder))
+	w.Write(builder.FinishedBytes())
+}
+
 func HandleRequests(db Database, scoutingServer server.ScoutingServer) {
 	scoutingServer.HandleFunc("/requests", unknown)
 	scoutingServer.Handle("/requests/request/all_matches", requestAllMatchesHandler{db})
@@ -883,4 +933,5 @@
 	scoutingServer.Handle("/requests/request/shift_schedule", requestShiftScheduleHandler{db})
 	scoutingServer.Handle("/requests/submit/submit_driver_ranking", SubmitDriverRankingHandler{db})
 	scoutingServer.Handle("/requests/submit/submit_actions", submitActionsHandler{db})
+	scoutingServer.Handle("/requests/delete/delete_2023_data_scouting", Delete2023DataScoutingHandler{db})
 }
diff --git a/scouting/webserver/requests/requests_test.go b/scouting/webserver/requests/requests_test.go
index c1fe860..20a63ca 100644
--- a/scouting/webserver/requests/requests_test.go
+++ b/scouting/webserver/requests/requests_test.go
@@ -7,6 +7,7 @@
 
 	"github.com/frc971/971-Robot-Code/scouting/db"
 	"github.com/frc971/971-Robot-Code/scouting/webserver/requests/debug"
+	"github.com/frc971/971-Robot-Code/scouting/webserver/requests/messages/delete_2023_data_scouting"
 	"github.com/frc971/971-Robot-Code/scouting/webserver/requests/messages/request_2023_data_scouting"
 	"github.com/frc971/971-Robot-Code/scouting/webserver/requests/messages/request_2023_data_scouting_response"
 	"github.com/frc971/971-Robot-Code/scouting/webserver/requests/messages/request_all_driver_rankings"
@@ -907,6 +908,112 @@
 	}
 }
 
+// Validates that we can delete stats.
+func TestDeleteFromStats(t *testing.T) {
+	database := MockDatabase{
+		stats2023: []db.Stats2023{
+			{
+				TeamNumber: "3634", MatchNumber: 1, SetNumber: 2,
+				CompLevel: "quals", StartingQuadrant: 3, LowCubesAuto: 10,
+				MiddleCubesAuto: 1, HighCubesAuto: 1, CubesDroppedAuto: 0,
+				LowConesAuto: 1, MiddleConesAuto: 2, HighConesAuto: 1,
+				ConesDroppedAuto: 0, LowCubes: 1, MiddleCubes: 1,
+				HighCubes: 2, CubesDropped: 1, LowCones: 1,
+				MiddleCones: 2, HighCones: 0, ConesDropped: 1, SuperchargedPieces: 0,
+				AvgCycle: 34, Mobility: false, DockedAuto: true, EngagedAuto: false,
+				BalanceAttemptAuto: false, Docked: false, Engaged: false,
+				BalanceAttempt: true, CollectedBy: "isaac",
+			},
+			{
+				TeamNumber: "2343", MatchNumber: 1, SetNumber: 2,
+				CompLevel: "quals", StartingQuadrant: 1, LowCubesAuto: 0,
+				MiddleCubesAuto: 1, HighCubesAuto: 1, CubesDroppedAuto: 2,
+				LowConesAuto: 0, MiddleConesAuto: 0, HighConesAuto: 0,
+				ConesDroppedAuto: 1, LowCubes: 0, MiddleCubes: 0,
+				HighCubes: 1, CubesDropped: 0, LowCones: 0,
+				MiddleCones: 2, HighCones: 1, ConesDropped: 1, SuperchargedPieces: 0,
+				AvgCycle: 53, Mobility: false, DockedAuto: false, EngagedAuto: false,
+				BalanceAttemptAuto: true, Docked: false, Engaged: false,
+				BalanceAttempt: true, CollectedBy: "unknown",
+			},
+		},
+		actions: []db.Action{
+			{
+				PreScouting:     true,
+				TeamNumber:      "3634",
+				MatchNumber:     1,
+				SetNumber:       2,
+				CompLevel:       "quals",
+				CollectedBy:     "debug_cli",
+				CompletedAction: []byte{},
+				Timestamp:       2400,
+			},
+			{
+				PreScouting:     true,
+				TeamNumber:      "2343",
+				MatchNumber:     1,
+				SetNumber:       2,
+				CompLevel:       "quals",
+				CollectedBy:     "debug_cli",
+				CompletedAction: []byte{},
+				Timestamp:       1009,
+			},
+		},
+	}
+	scoutingServer := server.NewScoutingServer()
+	HandleRequests(&database, scoutingServer)
+	scoutingServer.Start(8080)
+	defer scoutingServer.Stop()
+
+	builder := flatbuffers.NewBuilder(1024)
+	builder.Finish((&delete_2023_data_scouting.Delete2023DataScoutingT{
+		CompLevel:   "quals",
+		MatchNumber: 1,
+		SetNumber:   2,
+		TeamNumber:  "2343",
+	}).Pack(builder))
+
+	_, err := debug.Delete2023DataScouting("http://localhost:8080", builder.FinishedBytes())
+	if err != nil {
+		t.Fatal("Failed to delete from data scouting ", err)
+	}
+
+	expectedActions := []db.Action{
+		{
+			PreScouting:     true,
+			TeamNumber:      "3634",
+			MatchNumber:     1,
+			SetNumber:       2,
+			CompLevel:       "quals",
+			CollectedBy:     "debug_cli",
+			CompletedAction: []byte{},
+			Timestamp:       2400,
+		},
+	}
+
+	expectedStats := []db.Stats2023{
+		{
+			TeamNumber: "3634", MatchNumber: 1, SetNumber: 2,
+			CompLevel: "quals", StartingQuadrant: 3, LowCubesAuto: 10,
+			MiddleCubesAuto: 1, HighCubesAuto: 1, CubesDroppedAuto: 0,
+			LowConesAuto: 1, MiddleConesAuto: 2, HighConesAuto: 1,
+			ConesDroppedAuto: 0, LowCubes: 1, MiddleCubes: 1,
+			HighCubes: 2, CubesDropped: 1, LowCones: 1,
+			MiddleCones: 2, HighCones: 0, ConesDropped: 1, SuperchargedPieces: 0,
+			AvgCycle: 34, Mobility: false, DockedAuto: true, EngagedAuto: false,
+			BalanceAttemptAuto: false, Docked: false, Engaged: false,
+			BalanceAttempt: true, CollectedBy: "isaac",
+		},
+	}
+
+	if !reflect.DeepEqual(expectedActions, database.actions) {
+		t.Fatal("Expected ", expectedActions, ", but got:", database.actions)
+	}
+	if !reflect.DeepEqual(expectedStats, database.stats2023) {
+		t.Fatal("Expected ", expectedStats, ", but got:", database.stats2023)
+	}
+}
+
 // A mocked database we can use for testing. Add functionality to this as
 // needed for your tests.
 
@@ -995,3 +1102,29 @@
 func (database *MockDatabase) ReturnActions() ([]db.Action, error) {
 	return database.actions, nil
 }
+
+func (database *MockDatabase) DeleteFromStats(compLevel_ string, matchNumber_ int32, setNumber_ int32, teamNumber_ string) error {
+	for i, stat := range database.stats2023 {
+		if stat.CompLevel == compLevel_ &&
+			stat.MatchNumber == matchNumber_ &&
+			stat.SetNumber == setNumber_ &&
+			stat.TeamNumber == teamNumber_ {
+			// Match found, remove the element from the array.
+			database.stats2023 = append(database.stats2023[:i], database.stats2023[i+1:]...)
+		}
+	}
+	return nil
+}
+
+func (database *MockDatabase) DeleteFromActions(compLevel_ string, matchNumber_ int32, setNumber_ int32, teamNumber_ string) error {
+	for i, action := range database.actions {
+		if action.CompLevel == compLevel_ &&
+			action.MatchNumber == matchNumber_ &&
+			action.SetNumber == setNumber_ &&
+			action.TeamNumber == teamNumber_ {
+			// Match found, remove the element from the array.
+			database.actions = append(database.actions[:i], database.actions[i+1:]...)
+		}
+	}
+	return nil
+}
diff --git a/scouting/www/view/BUILD b/scouting/www/view/BUILD
index d787e8f..67c7e3b 100644
--- a/scouting/www/view/BUILD
+++ b/scouting/www/view/BUILD
@@ -10,6 +10,8 @@
     ],
     deps = [
         ":node_modules/@angular/forms",
+        "//scouting/webserver/requests/messages:delete_2023_data_scouting_response_ts_fbs",
+        "//scouting/webserver/requests/messages:delete_2023_data_scouting_ts_fbs",
         "//scouting/webserver/requests/messages:error_response_ts_fbs",
         "//scouting/webserver/requests/messages:request_2023_data_scouting_response_ts_fbs",
         "//scouting/webserver/requests/messages:request_2023_data_scouting_ts_fbs",
diff --git a/scouting/www/view/view.component.ts b/scouting/www/view/view.component.ts
index 561ae08..c75f83b 100644
--- a/scouting/www/view/view.component.ts
+++ b/scouting/www/view/view.component.ts
@@ -1,4 +1,6 @@
 import {Component, OnInit} from '@angular/core';
+import {Builder, ByteBuffer} from 'flatbuffers';
+import {ErrorResponse} from '../../webserver/requests/messages/error_response_generated';
 import {
   Ranking,
   RequestAllDriverRankingsResponse,
@@ -11,6 +13,8 @@
   Note,
   RequestAllNotesResponse,
 } from '../../webserver/requests/messages/request_all_notes_response_generated';
+import {Delete2023DataScouting} from '../../webserver/requests/messages/delete_2023_data_scouting_generated';
+import {Delete2023DataScoutingResponse} from '../../webserver/requests/messages/delete_2023_data_scouting_response_generated';
 
 import {ViewDataRequestor} from '../rpc';
 
@@ -104,16 +108,83 @@
   }
 
   // TODO(Filip): Add delete functionality.
-  // Gets called when a user clicks the delete icon.
-  async deleteData() {
+  // Gets called when a user clicks the delete icon (note scouting).
+  async deleteNoteData() {
     const block_alerts = document.getElementById(
       'block_alerts'
     ) as HTMLInputElement;
-    if (!block_alerts.checked) {
-      if (!window.confirm('Actually delete data?')) {
-        this.errorMessage = 'Deleting data has not been implemented yet.';
-        return;
-      }
+    if (block_alerts.checked || window.confirm('Actually delete data?')) {
+      this.errorMessage = 'Deleting data has not been implemented yet.';
+      return;
+    }
+  }
+
+  // TODO(Filip): Add delete functionality.
+  // Gets called when a user clicks the delete icon (driver ranking).
+  async deleteDriverRankingData() {
+    const block_alerts = document.getElementById(
+      'block_alerts'
+    ) as HTMLInputElement;
+    if (block_alerts.checked || window.confirm('Actually delete data?')) {
+      this.errorMessage = 'Deleting data has not been implemented yet.';
+      return;
+    }
+  }
+
+  // Gets called when a user clicks the delete icon.
+  async deleteDataScouting(
+    compLevel: string,
+    matchNumber: number,
+    setNumber: number,
+    teamNumber: string
+  ) {
+    const block_alerts = document.getElementById(
+      'block_alerts'
+    ) as HTMLInputElement;
+    if (block_alerts.checked || window.confirm('Actually delete data?')) {
+      await this.requestDeleteDataScouting(
+        compLevel,
+        matchNumber,
+        setNumber,
+        teamNumber
+      );
+      await this.fetchStats2023();
+    }
+  }
+
+  async requestDeleteDataScouting(
+    compLevel: string,
+    matchNumber: number,
+    setNumber: number,
+    teamNumber: string
+  ) {
+    this.progressMessage = 'Deleting data. Please be patient.';
+    const builder = new Builder();
+    const compLevelData = builder.createString(compLevel);
+    const teamNumberData = builder.createString(teamNumber);
+
+    builder.finish(
+      Delete2023DataScouting.createDelete2023DataScouting(
+        builder,
+        compLevelData,
+        matchNumber,
+        setNumber,
+        teamNumberData
+      )
+    );
+
+    const buffer = builder.asUint8Array();
+    const res = await fetch('/requests/delete/delete_2023_data_scouting', {
+      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}"`;
     }
   }
 
diff --git a/scouting/www/view/view.ng.html b/scouting/www/view/view.ng.html
index 667765f..2273217 100644
--- a/scouting/www/view/view.ng.html
+++ b/scouting/www/view/view.ng.html
@@ -11,12 +11,22 @@
   </button>
   <ul class="dropdown-menu">
     <li>
-      <a class="dropdown-item" href="#" (click)="switchDataSource('Notes')">
+      <a
+        class="dropdown-item"
+        href="#"
+        (click)="switchDataSource('Notes')"
+        id="notes_source_dropdown"
+      >
         Notes
       </a>
     </li>
     <li>
-      <a class="dropdown-item" href="#" (click)="switchDataSource('Stats2023')">
+      <a
+        class="dropdown-item"
+        href="#"
+        (click)="switchDataSource('Stats2023')"
+        id="stats_source_dropdown"
+      >
         Stats
       </a>
     </li>
@@ -25,6 +35,7 @@
         class="dropdown-item"
         href="#"
         (click)="switchDataSource('DriverRanking')"
+        id="driver_ranking_source_dropdown"
       >
         Driver Ranking
       </a>
@@ -64,7 +75,7 @@
           <td>{{parseKeywords(note)}}</td>
           <!-- Delete Icon. -->
           <td>
-            <button class="btn btn-danger" (click)="deleteData()">
+            <button class="btn btn-danger" (click)="deleteNoteData()">
               <i class="bi bi-trash"></i>
             </button>
           </td>
@@ -94,13 +105,17 @@
       </thead>
       <tbody>
         <tr *ngFor="let stat2023 of statList; index as i;">
-          <th scope="row">{{stat2023.match()}}</th>
-          <td>{{stat2023.team()}}</td>
+          <th scope="row">{{stat2023.matchNumber()}}</th>
+          <td>{{stat2023.teamNumber()}}</td>
           <td>{{stat2023.setNumber()}}</td>
           <td>{{COMP_LEVEL_LABELS[stat2023.compLevel()]}}</td>
           <!-- Delete Icon. -->
           <td>
-            <button class="btn btn-danger" (click)="deleteData()">
+            <button
+              class="btn btn-danger"
+              id="delete_button_{{i}}"
+              (click)="deleteDataScouting(stat2023.compLevel(), stat2023.matchNumber(), stat2023.setNumber(), stat2023.teamNumber())"
+            >
               <i class="bi bi-trash"></i>
             </button>
           </td>
@@ -136,7 +151,7 @@
           <td>{{ranking.rank3()}}</td>
           <!-- Delete Icon. -->
           <td>
-            <button class="btn btn-danger" (click)="deleteData()">
+            <button class="btn btn-danger" (click)="deleteDriverRankingData()">
               <i class="bi bi-trash"></i>
             </button>
           </td>