From ee7d23dd5c5b15a05f48e7ddc620e423630b825b Mon Sep 17 00:00:00 2001 From: renatodellosso Date: Sun, 16 Jun 2024 15:40:33 -0400 Subject: [PATCH 1/2] You can now create comps not linked to TBA --- lib/API.ts | 7 ++- lib/TheBlueAlliance.ts | 9 +++- lib/client/ClientUtils.ts | 4 +- pages/[teamSlug]/[seasonSlug]/createComp.tsx | 50 +++++++++----------- 4 files changed, 39 insertions(+), 31 deletions(-) diff --git a/lib/API.ts b/lib/API.ts index 2724f709..667dbe0d 100644 --- a/lib/API.ts +++ b/lib/API.ts @@ -800,7 +800,12 @@ export namespace API { }, teamCompRanking: async (req, res, { tba, data }) => { - const { rankings } = await tba.req.getCompetitonRanking(data.tbaId); + const tbaResult = await tba.req.getCompetitonRanking(data.tbaId); + if (!tbaResult || !tbaResult.rankings) { + return res.status(200).send({ place: "?", max: "?" }); + } + + const { rankings } = tbaResult; const rank = rankings.find((ranking) => ranking.team_key === `frc${data.team}`)?.rank; diff --git a/lib/TheBlueAlliance.ts b/lib/TheBlueAlliance.ts index 79f71f38..94837078 100644 --- a/lib/TheBlueAlliance.ts +++ b/lib/TheBlueAlliance.ts @@ -9,6 +9,7 @@ import { Alliance, Pitreport, } from "./Types"; +import { NotLinkedToTba } from "./client/ClientUtils"; export namespace TheBlueAlliance { export interface SimpleTeam { @@ -255,6 +256,9 @@ export namespace TheBlueAlliance { } async getCompetitionMatches(tbaId: string): Promise { + if (tbaId === NotLinkedToTba) + return []; + let matches = (await this.req.getCompetitionMatches(tbaId)).map( (data) => new Match( @@ -266,11 +270,14 @@ export namespace TheBlueAlliance { tbaIdsToTeamNumbers(data.alliances.blue.team_keys), tbaIdsToTeamNumbers(data.alliances.red.team_keys) ) - ); + ) ?? []; return matches; } async getCompetitionPitreports(tbaId: string): Promise { + if (tbaId === NotLinkedToTba) + return []; + const competitionTeams = await this.req.getCompetitionTeams(tbaId); return competitionTeams.map( ({ team_number }) => new Pitreport(team_number) diff --git a/lib/client/ClientUtils.ts b/lib/client/ClientUtils.ts index bb3322cb..4d68cb7e 100644 --- a/lib/client/ClientUtils.ts +++ b/lib/client/ClientUtils.ts @@ -4,4 +4,6 @@ export function getIdsInProgressFromTimestamps(timestamps: { [id: string]: strin const timestamp = timestamps[id]; return ((now - new Date(timestamp).getTime()) / 1000) < 10; }); -} \ No newline at end of file +} + +export const NotLinkedToTba = "not-linked"; \ No newline at end of file diff --git a/pages/[teamSlug]/[seasonSlug]/createComp.tsx b/pages/[teamSlug]/[seasonSlug]/createComp.tsx index 0ba8a2cf..f13d5012 100644 --- a/pages/[teamSlug]/[seasonSlug]/createComp.tsx +++ b/pages/[teamSlug]/[seasonSlug]/createComp.tsx @@ -1,4 +1,4 @@ -import { CompetitonNameIdPair } from "@/lib/Types"; +import { CompetitonNameIdPair as CompetitionNameIdPair } from "@/lib/Types"; import React, { useEffect, useState } from "react"; import ClientAPI from "@/lib/client/ClientAPI"; @@ -7,7 +7,7 @@ import { GetServerSideProps } from "next"; import Container from "@/components/Container"; import Flex from "@/components/Flex"; import Card from "@/components/Card"; -import Loading from "@/components/Loading"; +import { NotLinkedToTba } from "@/lib/client/ClientUtils"; const api = new ClientAPI("gearboxiscool"); @@ -17,7 +17,7 @@ export default function CreateComp(props: ResolvedUrlData) { const [name, setName] = useState(""); const [results, setResults] = useState< - { value: number; pair: CompetitonNameIdPair }[] + { value: number; pair: CompetitionNameIdPair }[] >([]); const [selection, setSelection] = useState(); const [loading, setLoading] = useState(false); @@ -43,18 +43,15 @@ export default function CreateComp(props: ResolvedUrlData) { const createComp = async () => { setCreatingComp(true); - if (selection === undefined) { - return; - } - const autofill = await api.getCompetitionAutofillData( + const autofill = selection ? await api.getCompetitionAutofillData( results[selection].pair.tbaId - ); + ) : undefined; const comp = await api.createCompetition( - autofill.name, - autofill.tbaId, - autofill.start, - autofill.end, + autofill?.name ?? name, + autofill?.tbaId ?? NotLinkedToTba, + autofill?.start ?? 0, + autofill?.end ?? 0, season?._id, usePublicData ); @@ -71,7 +68,7 @@ export default function CreateComp(props: ResolvedUrlData) {
-

Search for a competition

+

Search for a competition or enter details

- {selection !== undefined && selection >= 0 ? - (creatingComp - ? ( - - ) - : ( -
+ {creatingComp + ? ( + + ) + :
+ {(selection !== undefined && selection >= 0) && (

Make data publicly available?

@@ -130,14 +126,12 @@ export default function CreateComp(props: ResolvedUrlData) { onChange={(e) => setUsePublicData(e.target.checked)} />
- -
- )) - : ( - <> - )} + )} + +
+ } From d6d6cfa946511e9d955e6a12fab4c4a02bf25575 Mon Sep 17 00:00:00 2001 From: renatodellosso Date: Sun, 16 Jun 2024 22:10:01 -0400 Subject: [PATCH 2/2] Manual pit report adding adding --- components/stats/TeamPage.tsx | 43 +++++-- components/stats/TeamStats.tsx | 2 +- lib/API.ts | 33 ++++- lib/client/ClientAPI.ts | 5 + .../[seasonSlug]/[competitonSlug]/index.tsx | 118 +++++++++++------- .../[competitonSlug]/pitstats.tsx | 9 +- .../[seasonSlug]/[competitonSlug]/stats.tsx | 17 +-- pages/[teamSlug]/[seasonSlug]/createComp.tsx | 8 +- 8 files changed, 155 insertions(+), 80 deletions(-) diff --git a/components/stats/TeamPage.tsx b/components/stats/TeamPage.tsx index b2ec000b..360e8f3a 100644 --- a/components/stats/TeamPage.tsx +++ b/components/stats/TeamPage.tsx @@ -166,13 +166,13 @@ function TeamCard(props: { export default function TeamPage(props: { reports: Report[], pitReports: Pitreport[], subjectiveReports: SubjectiveReport[] }) { const reports = props.reports; - const pitReports: { [key: number]: Pitreport } = {}; + const [pitReports, setPitReports] = useState<{ [key: number]: Pitreport }>({}); const [teamReports, setTeamReports] = useState<{ [key: number]: Report[] }>( {} ); const [teamSubjectiveReports, setTeamSubjectiveReports] = useState<{ [key: number]: SubjectiveReport[] }>({}); - const teamNumbers = Object.keys(teamReports); + const teamNumbers = Array.from(new Set([...Object.keys(teamReports), ...Object.keys(pitReports), ...Object.keys(teamSubjectiveReports)])); const [selectedTeam, setSelectedTeam] = useState(); const selectedReports = teamReports[selectedTeam ? selectedTeam : 0]; @@ -187,6 +187,24 @@ export default function TeamPage(props: { reports: Report[], pitReports: Pitrepo } }); setTeamReports(newTeamReports); + + const newPitReports: typeof pitReports = {}; + props.pitReports.forEach((pitReport) => { + newPitReports[pitReport.teamNumber] = pitReport; + }); + setPitReports(newPitReports); + + const subjectiveReports: typeof teamSubjectiveReports = {}; + props.subjectiveReports.forEach((subjectiveReport) => { + for (const teamNumber of Object.keys(subjectiveReport.robotComments)) { + if (!Object.keys(subjectiveReports).includes(teamNumber)) { + subjectiveReports[Number(teamNumber)] = [subjectiveReport]; + } else { + subjectiveReports[Number(teamNumber)].push(subjectiveReport); + } + } + }); + setTeamSubjectiveReports(subjectiveReports); }; useEffect(() => { @@ -208,8 +226,9 @@ export default function TeamPage(props: { reports: Report[], pitReports: Pitrepo const stDev = StandardDeviation(pointTotals); useEffect(() => { - associateTeams(); - }, [reports]); + console.log("Associating teams..."); + associateTeams(); + }, [reports, props.pitReports, props.subjectiveReports]); // Associate pit reports props.pitReports.forEach((pitReport) => { @@ -219,24 +238,22 @@ export default function TeamPage(props: { reports: Report[], pitReports: Pitrepo const teamRanking = Object.keys(teamReports).sort((a, b) => { const a1 = AveragePoints(teamReports[Number(a)]); const b1 = AveragePoints(teamReports[Number(b)]); - if (a1 < b1) { - return 1; - } else if (a1 > b1) { - return -1; - } - return 0; + return b1 - a1; }); + // Find teams not in team ranking + const missingTeams = teamNumbers.filter((team) => !teamRanking.includes(team)); + return (
- {teamRanking.map((number, index) => ( + {teamRanking.concat(missingTeams).map((number, index) => ( setSelectedTeam(Number(number))} compAvgPoints={avgPoints} diff --git a/components/stats/TeamStats.tsx b/components/stats/TeamStats.tsx index ee0ea886..4ff9d040 100644 --- a/components/stats/TeamStats.tsx +++ b/components/stats/TeamStats.tsx @@ -74,7 +74,7 @@ export default function TeamStats(props: { } } - const commentList = props.selectedReports.filter((report) => report.data.comments.length > 0); + const commentList = props.selectedReports?.filter((report) => report.data.comments.length > 0) ?? []; if (commentList.length === 0) return setComments([]); const promises = commentList.map((report) => api.findMatchById(report.match).then((match) => addComment( diff --git a/lib/API.ts b/lib/API.ts index 667dbe0d..78914b20 100644 --- a/lib/API.ts +++ b/lib/API.ts @@ -497,7 +497,7 @@ export namespace API { // time // type // } - var match = await db.addObject( + const match = await db.addObject( Collections.Matches, new Match( data.number, @@ -509,16 +509,20 @@ export namespace API { data.blueAlliance ) ); - var comp = await db.findObjectById( + + const reportPromise = generateReportsForMatch(match); + + const comp = await db.findObjectById( Collections.Competitions, new ObjectId(data.compId) ); comp.matches.push(match._id ? String(match._id) : ""); - await db.updateObjectById( + + await Promise.all([db.updateObjectById( Collections.Competitions, new ObjectId(comp._id), comp - ); + ), reportPromise]); return res.status(200).send(match); }, @@ -983,7 +987,26 @@ export namespace API { subjectiveScouter: userId, }); return res.status(200).send({ result: "success" }); - } + }, + + createPitReportForTeam: async (req, res, { db, data }) => { + const { teamNumber, compId } = data; + const compPromise = db.findObjectById(Collections.Competitions, new ObjectId(compId)); + + const pitReport = new Pitreport(teamNumber); + const pitReportId = (await db.addObject(Collections.Pitreports, pitReport))._id?.toString(); + + if (!pitReportId) + return res.status(500).send({ error: "Failed to create pit report" }); + + (await compPromise).pitReports.push(pitReportId); + + await db.updateObjectById(Collections.Competitions, new ObjectId(compId), { + pitReports: (await compPromise).pitReports, + }); + + return res.status(200).send({ result: "success" }); + } }; } diff --git a/lib/client/ClientAPI.ts b/lib/client/ClientAPI.ts index 1595e3e5..be899a24 100644 --- a/lib/client/ClientAPI.ts +++ b/lib/client/ClientAPI.ts @@ -53,6 +53,7 @@ export default class ClientAPI { }, body: JSON.stringify(body), }); + return await rawResponse.json(); } @@ -435,4 +436,8 @@ export default class ClientAPI { return await this.request("/setSubjectiveScouterForMatch", { matchId, userId }); } + async createPitReportForTeam(teamNumber: number, compId: string) { + return await this.request("/createPitReportForTeam", { teamNumber, compId }); + } + } diff --git a/pages/[teamSlug]/[seasonSlug]/[competitonSlug]/index.tsx b/pages/[teamSlug]/[seasonSlug]/[competitonSlug]/index.tsx index a2285bbc..8d1cd27e 100644 --- a/pages/[teamSlug]/[seasonSlug]/[competitonSlug]/index.tsx +++ b/pages/[teamSlug]/[seasonSlug]/[competitonSlug]/index.tsx @@ -1,13 +1,10 @@ import UrlResolver, { ResolvedUrlData } from "@/lib/UrlResolver"; import { ChangeEvent, useEffect, useState } from "react"; -import Image from "next/image"; - import ClientAPI from "@/lib/client/ClientAPI"; import { GetServerSideProps } from "next"; import { AllianceColor, - Form, Match, MatchType, Pitreport, @@ -22,21 +19,16 @@ import { useCurrentSession } from "@/lib/client/useCurrentSession"; import { MdAutoGraph, MdCoPresent, - MdDriveEta, - MdInsertPhoto, MdQueryStats, } from "react-icons/md"; -import { BsClipboard2Check, BsGear, BsGearFill } from "react-icons/bs"; -import { FaBinoculars, FaDatabase, FaEdit, FaSync, FaUserCheck } from "react-icons/fa"; +import { BsClipboard2Check, BsGearFill } from "react-icons/bs"; +import { FaBinoculars, FaDatabase, FaSync, FaUserCheck } from "react-icons/fa"; import { FaCheck, FaRobot, FaUserGroup } from "react-icons/fa6"; import { Round } from "@/lib/client/StatsMath"; import Avatar from "@/components/Avatar"; -import { match } from "assert"; -import { report } from "process"; -import { useRouter } from "next/router"; import Loading from "@/components/Loading"; import useInterval from "@/lib/client/useInterval"; -import { getIdsInProgressFromTimestamps } from "@/lib/client/ClientUtils"; +import { NotLinkedToTba, getIdsInProgressFromTimestamps } from "@/lib/client/ClientUtils"; const api = new ClientAPI("gearboxiscool"); @@ -97,6 +89,8 @@ export default function Home(props: ResolvedUrlData) { const [matchBeingEdited, setMatchBeingEdited] = useState(); + const [teamToAdd, setTeamToAdd] = useState(); + const regeneratePitReports = async () => { console.log("Regenerating pit reports..."); api @@ -107,7 +101,7 @@ export default function Home(props: ResolvedUrlData) { // Fetch pit reports const pitReportPromises = pitReports.map( - async (id: string) => await api.findPitreportById(id) + api.findPitreportById ); Promise.all(pitReportPromises).then((reports) => { @@ -119,12 +113,10 @@ export default function Home(props: ResolvedUrlData) { }; useEffect(() => { - console.log("Checking if matches are assigned"); - let matchesAssigned = true; + for (const report of reports) { if (!report.user) { - console.log("No user assigned to report", report); matchesAssigned = false; break; } @@ -455,6 +447,14 @@ export default function Home(props: ResolvedUrlData) { api.remindSlack(slackId, session.user?.slackId); } + function addTeam() { + if (!teamToAdd || !comp?._id) return; + + api.createPitReportForTeam(teamToAdd, comp?._id).then(() => { + location.reload(); + }); + } + return (
@@ -472,18 +472,14 @@ export default function Home(props: ResolvedUrlData) {
Stats
Pit Stats @@ -526,14 +522,25 @@ export default function Home(props: ResolvedUrlData) {
{showSettings ? (
+ { + comp?.tbaId === NotLinkedToTba && <> +
+

This competition is not linked to TBA

+

Some features will be unavailable.

+
+
+ + }

Settings

- + { comp?.tbaId !== NotLinkedToTba && + + } + + { comp?.tbaId !== NotLinkedToTba && <> +
+
+

Make data public?

+ +
+

+ Making your data publicly available helps smaller teams make informed decisions during alliance selection. + Don't worry - no identifying information will be shared and comments will be hidden; only quantitative + data will be shared.
This setting can be changed at any time. +

+ }
@@ -978,7 +1006,9 @@ export default function Home(props: ResolvedUrlData) {

Pitscouting not available

-
Could not fetch team list from TBA
+
+ {comp?.tbaId !== NotLinkedToTba ? "Could not fetch team list from TBA" : "You'll need to manually add teams from Settings" } +
) : (
diff --git a/pages/[teamSlug]/[seasonSlug]/[competitonSlug]/pitstats.tsx b/pages/[teamSlug]/[seasonSlug]/[competitonSlug]/pitstats.tsx index 68106986..199cbfc1 100644 --- a/pages/[teamSlug]/[seasonSlug]/[competitonSlug]/pitstats.tsx +++ b/pages/[teamSlug]/[seasonSlug]/[competitonSlug]/pitstats.tsx @@ -18,6 +18,7 @@ import { Collections, getDatabase } from "@/lib/MongoDB"; import { NumericalAverage, StandardDeviation } from "@/lib/client/StatsMath"; import { TheBlueAlliance } from "@/lib/TheBlueAlliance"; +import { NotLinkedToTba } from "@/lib/client/ClientUtils"; const api = new ClientAPI("gearboxiscool"); @@ -210,9 +211,6 @@ function TeamSlide(props: { export default function Pitstats(props: { competition: Competition }) { const comp = props.competition; - const [reports, setReports] = useState([]); - const [pitReports, setPitReports] = useState({}); - const [teamReportPairs, setTeamReportPairs] = useState({}); const [teamStatPairs, setTeamStatPairs] = useState< TeamStatPair | undefined >(); @@ -389,10 +387,7 @@ export default function Pitstats(props: { competition: Competition }) { ); }); setSlides(newSlides); - setReports(newReports); - setTeamReportPairs(newPairs); setTeamStatPairs(newStatPairs); - setPitReports(newPits); }; useEffect(() => { @@ -445,7 +440,7 @@ export default function Pitstats(props: { competition: Competition }) { useEffect(() => { const msg = "Would you like to include public data? (Ok = Yes, Cancel = No)"; - setUsePublicData(confirm(msg)); + setUsePublicData(comp.tbaId !== NotLinkedToTba && confirm(msg)); }, []); return ( diff --git a/pages/[teamSlug]/[seasonSlug]/[competitonSlug]/stats.tsx b/pages/[teamSlug]/[seasonSlug]/[competitonSlug]/stats.tsx index f75c46cc..0463f99c 100644 --- a/pages/[teamSlug]/[seasonSlug]/[competitonSlug]/stats.tsx +++ b/pages/[teamSlug]/[seasonSlug]/[competitonSlug]/stats.tsx @@ -12,6 +12,7 @@ import { TimeString } from "@/lib/client/FormatTime"; import ClientAPI from "@/lib/client/ClientAPI"; import { team } from "slack"; +import { NotLinkedToTba } from "@/lib/client/ClientUtils"; const api = new ClientAPI("gearboxiscool"); @@ -50,7 +51,7 @@ export default function Stats(props: StatsPageProps) { .then((data) => setReports(data)), pitReports.length === 0 && api.getPitReports(props.competition.pitReports).then((data) => { - setPitReports(data); + setPitReports(data); }), api.getSubjectiveReportsForComp(props.competition._id!).then(setSubjectiveReports), ].flat(); @@ -77,14 +78,14 @@ export default function Stats(props: StatsPageProps) { notForMobile={true} >
- } -
(Click to toggle)
- {/*

Use public data?

diff --git a/pages/[teamSlug]/[seasonSlug]/createComp.tsx b/pages/[teamSlug]/[seasonSlug]/createComp.tsx index f13d5012..690d8f1d 100644 --- a/pages/[teamSlug]/[seasonSlug]/createComp.tsx +++ b/pages/[teamSlug]/[seasonSlug]/createComp.tsx @@ -47,11 +47,15 @@ export default function CreateComp(props: ResolvedUrlData) { results[selection].pair.tbaId ) : undefined; + const now = new Date().getTime(); + + console.log(autofill); + const comp = await api.createCompetition( autofill?.name ?? name, autofill?.tbaId ?? NotLinkedToTba, - autofill?.start ?? 0, - autofill?.end ?? 0, + autofill?.start ?? now, + autofill?.end ?? now, season?._id, usePublicData );