Add Pit Scouting Tab

Signed-off-by: Emily Markova <emily.markova@gmail.com>
Change-Id: Iede446546e20f2915bb53e134050b5025976da36
diff --git a/scouting/www/BUILD b/scouting/www/BUILD
index 6ecad54..55e9a8f 100644
--- a/scouting/www/BUILD
+++ b/scouting/www/BUILD
@@ -19,6 +19,7 @@
         "//scouting/www/entry",
         "//scouting/www/match_list",
         "//scouting/www/notes",
+        "//scouting/www/pit_scouting",
         "//scouting/www/shift_schedule",
         "//scouting/www/view",
     ],
diff --git a/scouting/www/app/app.module.ts b/scouting/www/app/app.module.ts
index 7b200d0..bf393e4 100644
--- a/scouting/www/app/app.module.ts
+++ b/scouting/www/app/app.module.ts
@@ -9,6 +9,7 @@
 import {ShiftScheduleModule} from '../shift_schedule';
 import {ViewModule} from '../view';
 import {DriverRankingModule} from '../driver_ranking';
+import {PitScoutingModule} from '../pit_scouting';
 
 @NgModule({
   declarations: [App],
@@ -21,6 +22,7 @@
     ShiftScheduleModule,
     DriverRankingModule,
     ViewModule,
+    PitScoutingModule,
   ],
   exports: [App],
   bootstrap: [App],
diff --git a/scouting/www/app/app.ng.html b/scouting/www/app/app.ng.html
index b7f1873..5f7ea9f 100644
--- a/scouting/www/app/app.ng.html
+++ b/scouting/www/app/app.ng.html
@@ -64,6 +64,15 @@
       View
     </a>
   </li>
+  <li class="nav-item">
+    <a
+      class="nav-link"
+      [class.active]="tabIs('Pit')"
+      (click)="switchTabToGuarded('Pit')"
+    >
+      Pit
+    </a>
+  </li>
 </ul>
 
 <ng-container [ngSwitch]="tab">
@@ -83,4 +92,5 @@
   <app-driver-ranking *ngSwitchCase="'DriverRanking'"></app-driver-ranking>
   <shift-schedule *ngSwitchCase="'ShiftSchedule'"></shift-schedule>
   <app-view *ngSwitchCase="'View'"></app-view>
+  <app-pit-scouting *ngSwitchCase="'Pit'"></app-pit-scouting>
 </ng-container>
diff --git a/scouting/www/app/app.ts b/scouting/www/app/app.ts
index c19895a..16e3197 100644
--- a/scouting/www/app/app.ts
+++ b/scouting/www/app/app.ts
@@ -6,7 +6,8 @@
   | 'Entry'
   | 'DriverRanking'
   | 'ShiftSchedule'
-  | 'View';
+  | 'View'
+  | 'Pit';
 
 // Ignore the guard for tabs that don't require the user to enter any data.
 const unguardedTabs: Tab[] = ['MatchList'];
diff --git a/scouting/www/pit_scouting/BUILD b/scouting/www/pit_scouting/BUILD
new file mode 100644
index 0000000..740dee1
--- /dev/null
+++ b/scouting/www/pit_scouting/BUILD
@@ -0,0 +1,18 @@
+load("@npm//:defs.bzl", "npm_link_all_packages")
+load("//tools/build_rules:js.bzl", "ng_pkg")
+
+npm_link_all_packages(name = "node_modules")
+
+ng_pkg(
+    name = "pit_scouting",
+    extra_srcs = [
+        "//scouting/www:app_common_css",
+    ],
+    deps = [
+        ":node_modules/@angular/forms",
+        "//scouting/webserver/requests/messages:error_response_ts_fbs",
+        "//scouting/webserver/requests/messages:submit_pit_image_response_ts_fbs",
+        "//scouting/webserver/requests/messages:submit_pit_image_ts_fbs",
+        "@com_github_google_flatbuffers//ts:flatbuffers_ts",
+    ],
+)
diff --git a/scouting/www/pit_scouting/package.json b/scouting/www/pit_scouting/package.json
new file mode 100644
index 0000000..e58fc51
--- /dev/null
+++ b/scouting/www/pit_scouting/package.json
@@ -0,0 +1,7 @@
+{
+	"name": "@org_frc971/scouting/www/pit_scouting",
+	"private": true,
+	"dependencies": {
+			"@angular/forms": "15.1.5"
+	}
+}
diff --git a/scouting/www/pit_scouting/pit_scouting.component.css b/scouting/www/pit_scouting/pit_scouting.component.css
new file mode 100644
index 0000000..b224d73
--- /dev/null
+++ b/scouting/www/pit_scouting/pit_scouting.component.css
@@ -0,0 +1,8 @@
+* {
+  padding: 10px;
+}
+
+button {
+  touch-action: manipulation;
+  margin-top: 1vh;
+}
diff --git a/scouting/www/pit_scouting/pit_scouting.component.ts b/scouting/www/pit_scouting/pit_scouting.component.ts
new file mode 100644
index 0000000..b74c119
--- /dev/null
+++ b/scouting/www/pit_scouting/pit_scouting.component.ts
@@ -0,0 +1,75 @@
+import {Component} from '@angular/core';
+import {Builder, ByteBuffer} from 'flatbuffers';
+import {ErrorResponse} from '../../webserver/requests/messages/error_response_generated';
+import {SubmitPitImage} from '../../webserver/requests/messages/submit_pit_image_generated';
+
+type Section = 'TeamSelection' | 'Data';
+
+interface Input {
+  teamNumber: number;
+  pitImage: HTMLImageElement;
+}
+
+@Component({
+  selector: 'app-pit-scouting',
+  templateUrl: './pit_scouting.ng.html',
+  styleUrls: ['../app/common.css', './pit_scouting.component.css'],
+})
+export class PitScoutingComponent {
+  section: Section = 'Data';
+
+  errorMessage = '';
+  teamNumber: number = 971;
+  pitImage: string = '';
+
+  async readFile(file): Promise<ArrayBuffer> {
+    return new Promise((resolve, reject) => {
+      let reader = new FileReader();
+      reader.addEventListener('loadend', (e) =>
+        resolve(e.target.result as ArrayBuffer)
+      );
+      reader.addEventListener('error', reject);
+      reader.readAsArrayBuffer(file);
+    });
+  }
+
+  async getImageData() {
+    const selectedFile = (<HTMLInputElement>document.getElementById('pitImage'))
+      .files[0];
+    return new Uint8Array(await this.readFile(selectedFile));
+  }
+
+  async submitData() {
+    const builder = new Builder();
+    const teamNumber = builder.createString(this.teamNumber.toString());
+    const pitImage = builder.createString(this.pitImage.toString());
+    const imageData = SubmitPitImage.createImageDataVector(
+      builder,
+      await this.getImageData()
+    );
+    builder.finish(
+      SubmitPitImage.createSubmitPitImage(
+        builder,
+        teamNumber,
+        pitImage,
+        imageData
+      )
+    );
+
+    const buffer = builder.asUint8Array();
+    const res = await fetch('/requests/submit/submit_pit_image', {
+      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}"`;
+    }
+    this.section = 'TeamSelection';
+    this.pitImage = '';
+  }
+}
diff --git a/scouting/www/pit_scouting/pit_scouting.module.ts b/scouting/www/pit_scouting/pit_scouting.module.ts
new file mode 100644
index 0000000..7d92f6c
--- /dev/null
+++ b/scouting/www/pit_scouting/pit_scouting.module.ts
@@ -0,0 +1,11 @@
+import {NgModule} from '@angular/core';
+import {CommonModule} from '@angular/common';
+import {FormsModule} from '@angular/forms';
+import {PitScoutingComponent} from './pit_scouting.component';
+
+@NgModule({
+  declarations: [PitScoutingComponent],
+  exports: [PitScoutingComponent],
+  imports: [CommonModule, FormsModule],
+})
+export class PitScoutingModule {}
diff --git a/scouting/www/pit_scouting/pit_scouting.ng.html b/scouting/www/pit_scouting/pit_scouting.ng.html
new file mode 100644
index 0000000..98aa313
--- /dev/null
+++ b/scouting/www/pit_scouting/pit_scouting.ng.html
@@ -0,0 +1,30 @@
+<div class="header">
+  <h2>Pit Scouting</h2>
+</div>
+<ng-container [ngSwitch]="section">
+  <div *ngSwitchCase="'Data'" id="pitImageSelection" class="container-fluid">
+    <label for="teamNumber">Team Number</label>
+    <input
+      [(ngModel)]="teamNumber"
+      type="string"
+      id="teamNumber"
+      min="1"
+      max="9999"
+    />
+    <form action="setPitImage()">
+      <label for="pitImage">Select pit image:</label>
+      <br />
+      <input
+        id="pitImage"
+        [(ngModel)]="pitImage"
+        type="file"
+        accept="image/*"
+      />
+    </form>
+    <button id="submit-button" class="btn btn-success" (click)="submitData()">
+      Submit
+    </button>
+  </div>
+
+  <span class="error_message" role="alert">{{ errorMessage }}</span>
+</ng-container>