From 3037d18cdb0344df092457f271da7d7eb41d2770 Mon Sep 17 00:00:00 2001 From: Kyle Flynn Date: Fri, 10 Aug 2018 16:31:18 -0500 Subject: [PATCH] Added match review and audience display fix. --- .../fgc_2018/match-play/MatchPlayScreen.css | 2 +- ems-core/package.electron.json | 2 +- ems-core/package.json | 2 +- ems-core/src/components/AppContainer.tsx | 3 + .../EnergyImpactBlueScorecard.tsx | 3 +- .../EnergyImpactRedScorecard.tsx | 3 +- .../views/match-play/containers/MatchPlay.tsx | 23 +- .../controllers/MatchFlowController.ts | 6 +- .../views/match-review/MatchReviewView.tsx | 207 ++++++++++++++++++ ems-core/src/views/reports/ReportsView.tsx | 22 +- .../reports/reports/OtherCompetingTeams.tsx | 94 ++++++++ package.json | 2 +- 12 files changed, 342 insertions(+), 27 deletions(-) create mode 100644 ems-core/src/views/match-review/MatchReviewView.tsx create mode 100644 ems-core/src/views/reports/reports/OtherCompetingTeams.tsx diff --git a/audience-display/src/displays/fgc_2018/match-play/MatchPlayScreen.css b/audience-display/src/displays/fgc_2018/match-play/MatchPlayScreen.css index 9ad1191..2245015 100644 --- a/audience-display/src/displays/fgc_2018/match-play/MatchPlayScreen.css +++ b/audience-display/src/displays/fgc_2018/match-play/MatchPlayScreen.css @@ -99,7 +99,7 @@ flex-direction: row; font-family: "Roboto", sans-serif; font-weight: bold; - font-size: 3.7vw; + font-size: 3.5vw; } #score-container-red { diff --git a/ems-core/package.electron.json b/ems-core/package.electron.json index 8e1a196..22b2962 100644 --- a/ems-core/package.electron.json +++ b/ems-core/package.electron.json @@ -2,7 +2,7 @@ "name": "ems-core", "author": "The Orange Alliance", "description": "An improved event experience.", - "version": "2.1.10", + "version": "2.2.1", "private": true, "main": "public/desktop/electron.js", "homepage": "./public/desktop/" diff --git a/ems-core/package.json b/ems-core/package.json index f664224..04827a5 100644 --- a/ems-core/package.json +++ b/ems-core/package.json @@ -1,6 +1,6 @@ { "name": "ems-core", - "version": "2.1.10", + "version": "2.2.1", "private": true, "main": "public/electron.js", "dependencies": { diff --git a/ems-core/src/components/AppContainer.tsx b/ems-core/src/components/AppContainer.tsx index 0eff600..ad7fd56 100644 --- a/ems-core/src/components/AppContainer.tsx +++ b/ems-core/src/components/AppContainer.tsx @@ -10,6 +10,7 @@ import MatchPlayView from "../views/match-play/MatchPlayView"; import MatchTestView from "../views/match-test/MatchTestView"; import AboutView from "../views/about/AboutView"; import ReportsView from "../views/reports/ReportsView"; +import MatchReviewView from "../views/match-review/MatchReviewView"; interface IProps { slaveMode?: boolean, @@ -69,6 +70,8 @@ class AppContainer extends React.Component { return ; case "Match Test": return ; + case "Match Review": + return ; case "Reports": return ; case "About": diff --git a/ems-core/src/components/game-specifics/EnergyImpactBlueScorecard.tsx b/ems-core/src/components/game-specifics/EnergyImpactBlueScorecard.tsx index 46bc279..7f59b15 100644 --- a/ems-core/src/components/game-specifics/EnergyImpactBlueScorecard.tsx +++ b/ems-core/src/components/game-specifics/EnergyImpactBlueScorecard.tsx @@ -260,5 +260,4 @@ export function mapDispatchToProps(dispatch: Dispatch) { }; } -export default connect(mapStateToProps, mapDispatchToProps)(EnergyImpactBlueScorecard as any); -// This is 100% not type safe, but TypeScript started randomly complaining and all other components are fine. \ No newline at end of file +export default connect(mapStateToProps, mapDispatchToProps)(EnergyImpactBlueScorecard); \ No newline at end of file diff --git a/ems-core/src/components/game-specifics/EnergyImpactRedScorecard.tsx b/ems-core/src/components/game-specifics/EnergyImpactRedScorecard.tsx index d109ccb..0291060 100644 --- a/ems-core/src/components/game-specifics/EnergyImpactRedScorecard.tsx +++ b/ems-core/src/components/game-specifics/EnergyImpactRedScorecard.tsx @@ -262,5 +262,4 @@ export function mapDispatchToProps(dispatch: Dispatch) { }; } -export default connect(mapStateToProps, mapDispatchToProps)(EnergyImpactRedScorecard as any); -// This is 100% not type safe, but TypeScript started randomly complaining and all other components are fine. \ No newline at end of file +export default connect(mapStateToProps, mapDispatchToProps)(EnergyImpactRedScorecard); \ No newline at end of file diff --git a/ems-core/src/views/match-play/containers/MatchPlay.tsx b/ems-core/src/views/match-play/containers/MatchPlay.tsx index a7eaa7d..de1f26c 100644 --- a/ems-core/src/views/match-play/containers/MatchPlay.tsx +++ b/ems-core/src/views/match-play/containers/MatchPlay.tsx @@ -54,7 +54,8 @@ interface IProps { interface IState { selectedLevel: TournamentLevels configModalOpen: boolean, - activeMatch: Match + activeMatch: Match, + committingScores: boolean } class MatchPlay extends React.Component { @@ -63,7 +64,8 @@ class MatchPlay extends React.Component { this.state = { selectedLevel: "Practice", configModalOpen: false, - activeMatch: this.props.activeMatch + activeMatch: this.props.activeMatch, + committingScores: false }; this.changeSelectedLevel = this.changeSelectedLevel.bind(this); this.changeSelectedMatch = this.changeSelectedMatch.bind(this); @@ -82,7 +84,7 @@ class MatchPlay extends React.Component { } public render() { - const {selectedLevel} = this.state; + const {selectedLevel, committingScores} = this.state; const {eventConfig, matchState, connected, matchDuration} = this.props; const fieldControl: number[] = (typeof eventConfig.fieldsControlled === "undefined" ? [1] : eventConfig.fieldsControlled); @@ -111,7 +113,7 @@ class MatchPlay extends React.Component { const disabledStates = MatchFlowController.getDisabledStates(this.props.matchState); const hasRedAlliance = typeof activeMatch.participants !== "undefined" && activeMatch.participants.filter((participant) => participant.station < 20).length > 0; const hasBlueAlliance = typeof activeMatch.participants !== "undefined" && activeMatch.participants.filter((participant) => participant.station >= 20).length > 0; - const canPrestart = activeMatch.matchKey.length > 0 && activeMatch.fieldNumber > 0 && typeof activeMatch.participants !== null && hasRedAlliance && hasBlueAlliance; + const canPrestart = activeMatch.matchKey.length > 0 && activeMatch.fieldNumber > 0 && typeof activeMatch.participants !== null && hasRedAlliance && hasBlueAlliance && connected; const hasPrestarted = matchState !== MatchState.PRESTART_READY && matchState !== MatchState.PRESTART_IN_PROGRESS && matchState !== MatchState.MATCH_ABORTED; const disMin = matchDuration.minutes() < 10 ? "0" + matchDuration.minutes().toString() : matchDuration.minutes().toString(); const disSec = matchDuration.seconds() < 10 ? "0" + matchDuration.seconds().toString() : matchDuration.seconds().toString(); @@ -139,7 +141,7 @@ class MatchPlay extends React.Component { - + @@ -246,11 +248,13 @@ class MatchPlay extends React.Component { private commitScores() { // Make sure all of our 'active' objects are on the same page. + this.setState({committingScores: true}); this.props.activeMatch.matchDetails = this.props.activeDetails; this.props.activeMatch.participants = this.props.activeParticipants; MatchFlowController.commitScores(this.props.activeMatch, this.props.eventConfig).then(() => { this.props.setNavigationDisabled(false); this.props.setMatchState(MatchState.PRESTART_READY); + this.setState({committingScores: false}); if (this.props.activeMatch.tournamentLevel >= 10) { MatchFlowController.checkForAdvancements(this.props.activeMatch.tournamentLevel, this.props.eventConfig.elimsFormat).then((matches: Match[]) => { if (this.props.elimsMatches.length < matches.length) { @@ -261,6 +265,7 @@ class MatchPlay extends React.Component { }); } }).catch((error: HttpError) => { + this.setState({committingScores: false}); DialogManager.showErrorBox(error); }); } @@ -272,13 +277,13 @@ class MatchPlay extends React.Component { private getMatchesByTournamentLevel(tournamentLevel: TournamentLevels): Match[] { // TODO - Only show fields that EMS controls switch (tournamentLevel) { case "Practice": - return this.props.practiceMatches; + return this.props.practiceMatches.filter(match => this.props.eventConfig.fieldsControlled.indexOf(match.fieldNumber) > -1); case "Qualification": - return this.props.qualificationMatches; + return this.props.qualificationMatches.filter(match => this.props.eventConfig.fieldsControlled.indexOf(match.fieldNumber) > -1); case "Finals": - return this.props.finalsMatches; + return this.props.finalsMatches.filter(match => this.props.eventConfig.fieldsControlled.indexOf(match.fieldNumber) > -1); case "Eliminations": - return this.props.elimsMatches; + return this.props.elimsMatches.filter(match => this.props.eventConfig.fieldsControlled.indexOf(match.fieldNumber) > -1); default: return []; } diff --git a/ems-core/src/views/match-play/controllers/MatchFlowController.ts b/ems-core/src/views/match-play/controllers/MatchFlowController.ts index 40dd5c5..62627c7 100644 --- a/ems-core/src/views/match-play/controllers/MatchFlowController.ts +++ b/ems-core/src/views/match-play/controllers/MatchFlowController.ts @@ -86,8 +86,10 @@ class MatchFlowController { }); }, 500); } else { - SocketProvider.send("commit-scores", match.matchKey); - resolve(); + setTimeout(() => { + SocketProvider.send("commit-scores", match.matchKey); + resolve(); + }, 250); } }).catch((error: any) => { reject(error); diff --git a/ems-core/src/views/match-review/MatchReviewView.tsx b/ems-core/src/views/match-review/MatchReviewView.tsx new file mode 100644 index 0000000..9b5d7c5 --- /dev/null +++ b/ems-core/src/views/match-review/MatchReviewView.tsx @@ -0,0 +1,207 @@ +import * as React from "react"; +import {Button, Card, Dimmer, DropdownProps, Form, Grid, Loader} from "semantic-ui-react"; +import GameSpecificScorecard from "../../components/GameSpecificScorecard"; +import EventConfiguration from "../../shared/models/EventConfiguration"; +import {ApplicationActions, IApplicationState} from "../../stores"; +import {connect} from "react-redux"; +import {PostQualConfig, TournamentLevels} from "../../shared/AppTypes"; +import Match from "../../shared/models/Match"; +import {getTheme} from "../../shared/AppTheme"; +import {SyntheticEvent} from "react"; +import MatchFlowController from "../match-play/controllers/MatchFlowController"; +import {ISetActiveDetails, ISetActiveMatch, ISetActiveParticipants} from "../../stores/scoring/types"; +import MatchDetails from "../../shared/models/MatchDetails"; +import MatchParticipant from "../../shared/models/MatchParticipant"; +import {Dispatch} from "redux"; +import {setActiveDetails, setActiveMatch, setActiveParticipants} from "../../stores/scoring/actions"; +import HttpError from "../../shared/models/HttpError"; +import DialogManager from "../../shared/managers/DialogManager"; +import {IDisableNavigation, ISetEliminationsMatches} from "../../stores/internal/types"; +import {disableNavigation, setEliminationsMatches} from "../../stores/internal/actions"; +import ConfirmActionModal from "../../components/ConfirmActionModal"; + +interface IProps { + eventConfig?: EventConfiguration, + practiceMatches?: Match[], + qualificationMatches?: Match[], + finalsMatches?: Match[], + elimsMatches?: Match[], + activeMatch?: Match, + activeDetails?: MatchDetails, + activeParticipants?: MatchParticipant[], + setActiveMatch?: (match: Match) => ISetActiveMatch, + setActiveDetails?: (details: MatchDetails) => ISetActiveDetails, + setActiveParticipants?: (participants: MatchParticipant[]) => ISetActiveParticipants, + setNavigationDisabled?: (disabled: boolean) => IDisableNavigation, + setEliminationsMatches?: (match: Match[]) => ISetEliminationsMatches +} + +interface IState { + selectedLevel: TournamentLevels, + updatingScores: boolean, + confirmModalOpen: boolean +} + +class MatchReviewView extends React.Component { + constructor(props: IProps) { + super(props); + this.state = { + selectedLevel: "Practice", + updatingScores: false, + confirmModalOpen: false + }; + this.changeSelectedLevel = this.changeSelectedLevel.bind(this); + this.changeSelectedMatch = this.changeSelectedMatch.bind(this); + this.openConfirmModal = this.openConfirmModal.bind(this); + this.closeConfirmModal = this.closeConfirmModal.bind(this); + this.updateScores = this.updateScores.bind(this); + } + + public render() { + const {eventConfig, activeMatch} = this.props; + const {selectedLevel, updatingScores, confirmModalOpen} = this.state; + + const availableLevels = this.getAvailableTournamentLevels(eventConfig.postQualConfig).map(tournamentLevel => { + return { + text: tournamentLevel, + value: tournamentLevel + }; + }); + + const availableMatches = this.getMatchesByTournamentLevel(selectedLevel).map(match => { + return { + text: match.matchName, + value: match.matchKey + }; + }); + const selectedMatch: Match = activeMatch === null ? new Match() : activeMatch; + return ( +
+ + + + + + + + + + + + + + + + + + + + +
+ ); + } + + private openConfirmModal() { + this.setState({confirmModalOpen: true}); + } + + private closeConfirmModal() { + this.setState({confirmModalOpen: false}); + } + + private getAvailableTournamentLevels(postQualConfig: PostQualConfig): TournamentLevels[] { + return ["Practice", "Qualification", postQualConfig === "elims" ? "Eliminations" : "Finals"]; + } + + private getMatchesByTournamentLevel(tournamentLevel: TournamentLevels): Match[] { // TODO - Only show fields that EMS controls + switch (tournamentLevel) { + case "Practice": + return this.props.practiceMatches.filter(match => this.props.eventConfig.fieldsControlled.indexOf(match.fieldNumber) > -1); + case "Qualification": + return this.props.qualificationMatches.filter(match => this.props.eventConfig.fieldsControlled.indexOf(match.fieldNumber) > -1); + case "Finals": + return this.props.finalsMatches.filter(match => this.props.eventConfig.fieldsControlled.indexOf(match.fieldNumber) > -1); + case "Eliminations": + return this.props.elimsMatches.filter(match => this.props.eventConfig.fieldsControlled.indexOf(match.fieldNumber) > -1); + default: + return []; + } + } + + private changeSelectedLevel(event: SyntheticEvent, props: DropdownProps) { + const matches = this.getMatchesByTournamentLevel((props.value as TournamentLevels)); + if (matches.length > 0) { + this.props.setActiveMatch(matches[0]); + this.setState({ + selectedLevel: (props.value as TournamentLevels), + }); + } else { + this.setState({ + selectedLevel: (props.value as TournamentLevels) + }); + } + } + + private changeSelectedMatch(event: SyntheticEvent, props: DropdownProps) { + for (const match of this.getMatchesByTournamentLevel(this.state.selectedLevel)) { + if (match.matchKey === (props.value as string)) { + this.props.setActiveMatch(match); + // Temporarily set the match to what we have now, and then get ALL the details. + MatchFlowController.getMatchResults(match.matchKey).then((data: Match) => { + this.props.setActiveMatch(data); + this.props.setActiveParticipants(data.participants); + this.props.setActiveDetails(data.matchDetails); + }); + break; + } + } + } + + private updateScores() { + // Make sure all of our 'active' objects are on the same page. + this.setState({updatingScores: true, confirmModalOpen: false}); + this.props.activeMatch.matchDetails = this.props.activeDetails; + this.props.activeMatch.participants = this.props.activeParticipants; + MatchFlowController.commitScores(this.props.activeMatch, this.props.eventConfig).then(() => { + this.props.setNavigationDisabled(false); + this.setState({updatingScores: false}); + if (this.props.activeMatch.tournamentLevel >= 10) { + MatchFlowController.checkForAdvancements(this.props.activeMatch.tournamentLevel, this.props.eventConfig.elimsFormat).then((matches: Match[]) => { + if (this.props.elimsMatches.length < matches.length) { + this.props.setEliminationsMatches(matches); + } + }).catch((error: HttpError) => { + console.error(error); + }); + } + }).catch((error: HttpError) => { + this.setState({updatingScores: false}); + DialogManager.showErrorBox(error); + }); + } +} + +export function mapStateToProps({configState, internalState, scoringState}: IApplicationState) { + return { + eventConfig: configState.eventConfiguration, + practiceMatches: internalState.practiceMatches, + qualificationMatches: internalState.qualificationMatches, + finalsMatches: internalState.finalsMatches, + elimsMatches: internalState.eliminationsMatches, + activeMatch: scoringState.activeMatch, + activeDetails: scoringState.activeDetails, + activeParticipants: scoringState.activeParticipants + }; +} + +export function mapDispatchToProps(dispatch: Dispatch) { + return { + setActiveMatch: (match: Match) => dispatch(setActiveMatch(match)), + setActiveDetails: (details: MatchDetails) => dispatch(setActiveDetails(details)), + setActiveParticipants: (participants: MatchParticipant[]) => dispatch(setActiveParticipants(participants)), + setNavigationDisabled: (disabled: boolean) => dispatch(disableNavigation(disabled)), + setEliminationsMatches: (matches: Match[]) => dispatch(setEliminationsMatches(matches)) + }; +} + +export default connect(mapStateToProps, mapDispatchToProps)(MatchReviewView); \ No newline at end of file diff --git a/ems-core/src/views/reports/ReportsView.tsx b/ems-core/src/views/reports/ReportsView.tsx index 7ac1902..b0f0eb6 100644 --- a/ems-core/src/views/reports/ReportsView.tsx +++ b/ems-core/src/views/reports/ReportsView.tsx @@ -15,6 +15,7 @@ import EliminationsScheduleByTeam from "./reports/EliminationsScheduleByTeam"; import FinalsScheduleByTeam from "./reports/FinalsScheduleByTeam"; import QualificationRankings from "./reports/QualificationRankings"; import DialogManager from "../../shared/managers/DialogManager"; +import OtherCompetingTeams from "./reports/OtherCompetingTeams"; interface IProps { eventConfig?: EventConfiguration, @@ -51,6 +52,7 @@ class ReportsView extends React.Component { this.generateQualificationScheduleByTeam = this.generateQualificationScheduleByTeam.bind(this); this.generatePostQualScheduleByTeam = this.generatePostQualScheduleByTeam.bind(this); this.generateQualificationRankings = this.generateQualificationRankings.bind(this); + this.generateCompetingTeams = this.generateCompetingTeams.bind(this); } public render() { @@ -97,8 +99,8 @@ class ReportsView extends React.Component { - - + + @@ -134,8 +136,8 @@ class ReportsView extends React.Component { } - - + + @@ -147,14 +149,14 @@ class ReportsView extends React.Component { - + { eventConfig.postQualConfig === "elims" && - + } { eventConfig.postQualConfig === "finals" && - + } @@ -165,7 +167,7 @@ class ReportsView extends React.Component { - + @@ -266,6 +268,10 @@ class ReportsView extends React.Component { private generateQualificationRankings() { this.generateReport(); } + + private generateCompetingTeams() { + this.generateReport() + } } export function mapStateToProps({configState, internalState}: IApplicationState) { diff --git a/ems-core/src/views/reports/reports/OtherCompetingTeams.tsx b/ems-core/src/views/reports/reports/OtherCompetingTeams.tsx new file mode 100644 index 0000000..34806d0 --- /dev/null +++ b/ems-core/src/views/reports/reports/OtherCompetingTeams.tsx @@ -0,0 +1,94 @@ +import * as React from "react"; +import Team from "../../../shared/models/Team"; +import EventConfiguration from "../../../shared/models/EventConfiguration"; +import ReportTemplate from "./ReportTemplate"; +import EMSProvider from "../../../shared/providers/EMSProvider"; +import {AxiosResponse} from "axios"; +import HttpError from "../../../shared/models/HttpError"; +import DialogManager from "../../../shared/managers/DialogManager"; +import {Table} from "semantic-ui-react"; + +interface IProps { + eventConfig?: EventConfiguration, + onHTMLUpdate: (htmlStr: string) => void +} + +interface IState { + generated: boolean, + teams: Team[] +} + +class OtherCompetingTeams extends React.Component { + constructor(props: IProps) { + super(props); + this.state = { + generated: false, + teams: [] + }; + } + + public componentDidMount() { + EMSProvider.getTeams().then((teamRes: AxiosResponse) => { + const teams: Team[] = []; + if (teamRes.data && teamRes.data.payload && teamRes.data.payload.length > 0) { + for (const teamJSON of teamRes.data.payload) { + teams.push(new Team().fromJSON(teamJSON)); + } + } + this.setState({generated: true, teams: teams}); + }).catch((error: HttpError) => { + DialogManager.showErrorBox(error); + this.setState({generated: true}); + }); + } + + // TODO - We already have game-specific rank tables... Use them? + public render() { + const {onHTMLUpdate} = this.props; + const {generated, teams} = this.state; + const teamsView = teams.map(team => { + return ( + + {team.teamKey} + {team.teamNameShort} + {team.teamNameLong} + {team.robotName} + {team.location} + {team.country} + {team.countryCode} + + ); + }); + let view = ( + + + + Team ID + Name (Short) + Name (Long) + Robot Name + Location + Country + ISO 2 Code + + + + {teamsView} + +
+ ); + if (teams.length <= 0) { + view = (There are no teams to report.); + } + return ( + + ); + } +} + +export default OtherCompetingTeams; \ No newline at end of file diff --git a/package.json b/package.json index 4349950..5d70622 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "event-management-system", "description": "Hub for all programs in the EMS suite.", - "version": "2.1.10", + "version": "2.2.1", "private": true, "scripts": { "api": "cd ems-api/ && npm start",