diff --git a/.vscode/tasks.json b/.vscode/tasks.json
index 2985d4ab..fd663175 100644
--- a/.vscode/tasks.json
+++ b/.vscode/tasks.json
@@ -7,7 +7,7 @@
"kind": "build",
"isDefault": true
},
- "type": "shell",
+ "type": "shell",
"command": "turbo run build --filter=${input:project}-frontend --filter=${input:project}-backend"
},
{
diff --git a/apps/scouting/backend/build.ts b/apps/scouting/backend/build.ts
index 538ac1be..0b572042 100644
--- a/apps/scouting/backend/build.ts
+++ b/apps/scouting/backend/build.ts
@@ -2,7 +2,7 @@
import { build, context } from "esbuild";
import { spawn } from "child_process";
-const isDev = process.env.NODE_ENV === "DEV";
+const isDev = process.env.NODE_ENV !== "DEV";
const bundlePath = "dist/bundle.js";
diff --git a/apps/scouting/backend/src/routes/compare-router.ts b/apps/scouting/backend/src/routes/compare-router.ts
new file mode 100644
index 00000000..7d13e842
--- /dev/null
+++ b/apps/scouting/backend/src/routes/compare-router.ts
@@ -0,0 +1,140 @@
+//בס"ד
+
+import type { ScoutingForm, TeleClimbLevel } from "@repo/scouting_types";
+import { Router } from "express";
+import { getFormsCollection } from "./forms-router";
+import { pipe } from "fp-ts/lib/function";
+import {
+ flatMap,
+ left,
+ map,
+ right,
+ tryCatch,
+ fold,
+} from "fp-ts/lib/TaskEither";
+import { mongofyQuery } from "../middleware/query";
+import { StatusCodes } from "http-status-codes";
+import { calculateSum, firstElement, isEmpty } from "@repo/array-functions";
+import { calcAverageGeneralFuelData } from "./general-router";
+import { generalCalculateFuel } from "../fuel/fuel-general";
+import { getBPSes } from "./teams-router";
+import { averageFuel } from "../fuel/distance-split";
+
+export const compareRouter = Router();
+
+type GamePeriod = "auto" | "fullGame";
+
+const DIGITS_AFTER_DOT = 2;
+const INITIAL_COUNTER_VALUE = 0;
+const INCREMENT = 1;
+
+const calculateAverageScoredFuel = (
+ forms: ScoutingForm[],
+ gamePeriod: GamePeriod,
+) => {
+ const generalFuelData = forms.map((form) =>
+ generalCalculateFuel(form, getBPSes()),
+ );
+ const averagedFuelData = calcAverageGeneralFuelData(generalFuelData);
+
+ return parseFloat(
+ averagedFuelData[gamePeriod].scored.toFixed(DIGITS_AFTER_DOT),
+ );
+};
+
+const findMaxClimbLevel = (forms: ScoutingForm[]) => {
+ const fullGameClimbedLevels = [
+ ...forms.map((form) => form.tele.climb.level),
+ ...forms.map((form) => form.auto.climb.level),
+ ];
+
+ return fullGameClimbedLevels.includes("L3")
+ ? "L3"
+ : fullGameClimbedLevels.includes("L2")
+ ? "L2"
+ : fullGameClimbedLevels.includes("L1")
+ ? "L1"
+ : "L0";
+};
+
+const findTimesClimbedToMax = (
+ forms: ScoutingForm[],
+ maxLevel: TeleClimbLevel,
+) => {
+ return calculateSum(forms, (form) =>
+ form.tele.climb.level === maxLevel ? INCREMENT : INITIAL_COUNTER_VALUE,
+ );
+};
+
+const findTimesClimbedInAuto = (forms: ScoutingForm[]) => {
+ return calculateSum(forms, (form) =>
+ form.auto.climb.level === "L1" ? INCREMENT : INITIAL_COUNTER_VALUE,
+ );
+};
+
+const timesClimedToLevel = (
+ level: TeleClimbLevel,
+ climbedLevels: TeleClimbLevel[],
+) => climbedLevels.filter((currentLevel) => currentLevel === level).length;
+
+const findTimesClimbedToLevels = (forms: ScoutingForm[]) => {
+ const climbedLevels = forms.map((form) => form.tele.climb.level);
+ return {
+ L1: timesClimedToLevel("L1", climbedLevels),
+ L2: timesClimedToLevel("L2", climbedLevels),
+ L3: timesClimedToLevel("L3", climbedLevels),
+ };
+};
+
+
+compareRouter.get("/", async (req, res) => {
+ await pipe(
+ getFormsCollection(),
+ flatMap((collection) =>
+ tryCatch(
+ () => collection.find(mongofyQuery(req.query)).toArray(),
+ (error) => ({
+ status: StatusCodes.INTERNAL_SERVER_ERROR,
+ reason: `DB Error: ${error}`,
+ }),
+ ),
+ ),
+ flatMap((forms) => {
+ if (isEmpty(forms)) return right(forms);
+
+ const firstTeam = firstElement(forms).teamNumber;
+ const isSameTeam = forms.every((f) => f.teamNumber === firstTeam);
+
+ return isSameTeam
+ ? right(forms)
+ : left({
+ status: StatusCodes.BAD_REQUEST,
+ reason:
+ "Compare Two Validation Error: Forms contain data from multiple different teams.",
+ });
+ }),
+ map((teamForms) => ({
+ teamNumber: firstElement(teamForms).teamNumber,
+ averageFuel: {
+ averageFuelInGame: calculateAverageScoredFuel(teamForms, "fullGame"),
+ averageFuelInAuto: calculateAverageScoredFuel(teamForms, "auto"),
+ },
+ climb: {
+ maxClimbLevel: findMaxClimbLevel(teamForms),
+ timesClimbedToMax: findTimesClimbedToMax(
+ teamForms,
+ findMaxClimbLevel(teamForms),
+ ),
+ timesClimbedInAuto: findTimesClimbedInAuto(teamForms),
+ timesClimbedToLevels: findTimesClimbedToLevels(teamForms),
+ },
+ })),
+
+ fold(
+ (error) => () =>
+ Promise.resolve(res.status(error.status).send(error.reason)),
+ (teamCompareData) => () =>
+ Promise.resolve(res.status(StatusCodes.OK).json({ teamCompareData })),
+ ),
+ )();
+});
diff --git a/apps/scouting/backend/src/routes/forms-router.ts b/apps/scouting/backend/src/routes/forms-router.ts
index fef64eb5..db71b6a7 100644
--- a/apps/scouting/backend/src/routes/forms-router.ts
+++ b/apps/scouting/backend/src/routes/forms-router.ts
@@ -3,7 +3,7 @@
import { Router } from "express";
import { flow, pipe } from "fp-ts/lib/function";
import { getDb } from "../middleware/db";
-import { flatMap, fold, fromEither, map } from "fp-ts/lib/TaskEither";
+import { flatMap, fold, fromEither, map, tryCatch } from "fp-ts/lib/TaskEither";
import { scoutingFormCodec, type ScoutingForm } from "@repo/scouting_types";
import { StatusCodes } from "http-status-codes";
import { createBodyVerificationPipe } from "../middleware/verification";
@@ -57,3 +57,28 @@ formsRouter.post("/", async (req, res) => {
),
)();
});
+
+formsRouter.get("/teams", async (req, res) => {
+ await pipe(
+ getFormsCollection(),
+ flatMap((collection) =>
+ tryCatch(
+ () =>
+ collection
+ .find(mongofyQuery({ "match.type": "qualification" }))
+ .toArray(),
+ (error) => ({
+ status: StatusCodes.INTERNAL_SERVER_ERROR,
+ reason: `DB Error: ${error}`,
+ }),
+ ),
+ ),
+ map((forms) => forms.map((form) => form.teamNumber)),
+ fold(
+ (error) => () =>
+ Promise.resolve(res.status(error.status).send(error.reason)),
+ (teamNumbers) => () =>
+ Promise.resolve(res.status(StatusCodes.OK).json({ teamNumbers })),
+ ),
+ )();
+});
diff --git a/apps/scouting/backend/src/routes/general-router.ts b/apps/scouting/backend/src/routes/general-router.ts
index 8bfb4e66..ce8cb0a1 100644
--- a/apps/scouting/backend/src/routes/general-router.ts
+++ b/apps/scouting/backend/src/routes/general-router.ts
@@ -1,5 +1,4 @@
//בס"ד
-/* eslint-disable @typescript-eslint/no-magic-numbers */ //for the example bps
import { Router } from "express";
import { getFormsCollection } from "./forms-router";
@@ -8,10 +7,10 @@ import { flatMap, fold, map, tryCatch } from "fp-ts/lib/TaskEither";
import { mongofyQuery } from "../middleware/query";
import { generalCalculateFuel } from "../fuel/fuel-general";
import { StatusCodes } from "http-status-codes";
-
import type { BPS, FuelObject, GeneralFuelData } from "@repo/scouting_types";
import { averageFuel } from "../fuel/distance-split";
import { firstElement, isEmpty } from "@repo/array-functions";
+import { getBPSes } from "./teams-router";
export const generalRouter = Router();
@@ -21,13 +20,9 @@ interface AccumulatedFuelData {
tele: FuelObject[];
}
-const getBPS = () => {
- return [];
-};
-
const ONE_ITEM_ARRAY = 1;
-const calcAverageGeneralFuelData = (fuelData: GeneralFuelData[]) => {
+export const calcAverageGeneralFuelData = (fuelData: GeneralFuelData[]) => {
if (fuelData.length === ONE_ITEM_ARRAY || isEmpty(fuelData)) {
return firstElement(fuelData);
}
@@ -70,7 +65,7 @@ generalRouter.get("/", async (req, res) => {
map((forms) =>
forms.map((form) => ({
teamNumber: form.teamNumber,
- generalFuelData: generalCalculateFuel(form, getBPS()),
+ generalFuelData: generalCalculateFuel(form, getBPSes()),
})),
),
@@ -79,7 +74,7 @@ generalRouter.get("/", async (req, res) => {
(accumulatorRecord, fuelData) => ({
...accumulatorRecord,
[fuelData.teamNumber]: [
- ...accumulatorRecord[fuelData.teamNumber],
+ ...(accumulatorRecord[fuelData.teamNumber] ?? []),
fuelData.generalFuelData,
],
}),
diff --git a/apps/scouting/backend/src/routes/index.ts b/apps/scouting/backend/src/routes/index.ts
index 031242d4..bb94b519 100644
--- a/apps/scouting/backend/src/routes/index.ts
+++ b/apps/scouting/backend/src/routes/index.ts
@@ -6,6 +6,7 @@ import { gameRouter } from "./game-router";
import { formsRouter } from "./forms-router";
import { teamsRouter } from "./teams-router";
import { generalRouter } from "./general-router";
+import { compareRouter } from "./compare-router";
export const apiRouter = Router();
@@ -14,6 +15,7 @@ apiRouter.use("/tba", tbaRouter);
apiRouter.use("/game", gameRouter);
apiRouter.use("/team",teamsRouter)
apiRouter.use("/general", generalRouter);
+apiRouter.use("/compare", compareRouter);
apiRouter.get("/health", (req, res) => {
res.status(StatusCodes.OK).send({ message: "Healthy!" });
diff --git a/apps/scouting/backend/src/routes/teams-router.ts b/apps/scouting/backend/src/routes/teams-router.ts
index 1558ff0a..4e8c905a 100644
--- a/apps/scouting/backend/src/routes/teams-router.ts
+++ b/apps/scouting/backend/src/routes/teams-router.ts
@@ -116,7 +116,7 @@ const processTeam = (bpses: BPS[], forms: ScoutingForm[]): TeamData => {
return { tele, auto, fullGame, metrics: { epa: 0, bps: 0 } };
};
-const getBPSes = (): BPS[] => [
+export const getBPSes = (): BPS[] => [
{
events: [
{
diff --git a/apps/scouting/frontend/src/App.tsx b/apps/scouting/frontend/src/App.tsx
index 5be893b5..49c44212 100644
--- a/apps/scouting/frontend/src/App.tsx
+++ b/apps/scouting/frontend/src/App.tsx
@@ -3,12 +3,21 @@ import type { FC } from "react";
import { ScoutMatch } from "./scouter/pages/ScoutMatch";
import { Route, Routes } from "react-router-dom";
import { ScoutedMatches } from "./scouter/pages/ScoutedMatches";
+import { CompareTwo } from "./strategy/CompareTwo";
+import { GeneralDataTable } from "./strategy/GeneralDataTable";
const App: FC = () => {
return (