diff --git a/projects/gameboard-ui/src/app/admin/components/game-center/game-center-teams/game-center-teams.component.html b/projects/gameboard-ui/src/app/admin/components/game-center/game-center-teams/game-center-teams.component.html index 0b5f527b..fa0c3f99 100644 --- a/projects/gameboard-ui/src/app/admin/components/game-center/game-center-teams/game-center-teams.component.html +++ b/projects/gameboard-ui/src/app/admin/components/game-center/game-center-teams/game-center-teams.component.html @@ -20,7 +20,7 @@ diff --git a/projects/gameboard-ui/src/app/admin/components/game-center/game-center-teams/game-center-teams.component.ts b/projects/gameboard-ui/src/app/admin/components/game-center/game-center-teams/game-center-teams.component.ts index fb641af3..99248435 100644 --- a/projects/gameboard-ui/src/app/admin/components/game-center/game-center-teams/game-center-teams.component.ts +++ b/projects/gameboard-ui/src/app/admin/components/game-center/game-center-teams/game-center-teams.component.ts @@ -9,7 +9,7 @@ import { ToastService } from '@/utility/services/toast.service'; import { AdminEnrollTeamModalComponent } from '../../admin-enroll-team-modal/admin-enroll-team-modal.component'; import { ManageManualChallengeBonusesModalComponent } from '../../manage-manual-challenge-bonuses-modal/manage-manual-challenge-bonuses-modal.component'; import { SimpleEntity } from '@/api/models'; -import { GameCenterTeamsAdvancementFilter, GameCenterTeamSessionStatus, GameCenterTeamsResults, GameCenterTeamsSort } from '../game-center.models'; +import { GameCenterTeamsAdvancementFilter, GameCenterTeamSessionStatus, GameCenterTeamsResults, GameCenterTeamsResultsTeam, GameCenterTeamsSort } from '../game-center.models'; import { UnsubscriberService } from '@/services/unsubscriber.service'; import { unique } from '@/../tools/tools'; import { ScoreboardTeamDetailModalComponent } from '@/scoreboard/components/scoreboard-team-detail-modal/scoreboard-team-detail-modal.component'; @@ -90,49 +90,61 @@ export class GameCenterTeamsComponent implements OnInit { await this.load(this.game?.id); } - protected async handleDeployGameResources() { - if (!this.results || !this.gameId) + protected async handleConfirmDeployGameResources() { + const teams = this.resolveSelectedTeams(); + const nowish = this.nowService.nowToMsEpoch(); + + if (!teams.length) { + this.modalService.openConfirm({ + title: "No eligible teams", + bodyContent: "There are no teams eligible for deployment." + }); + return; + } - const teamIds = this.selectedTeamIds.length ? this.selectedTeamIds : this.results.teams.items.map(t => t.id); - const invalidTeamNames: string[] = []; - const validTeamIds: string[] = []; + const eligibleTeams: GameCenterTeamsResultsTeam[] = []; + const ineligibleTeams: GameCenterTeamsResultsTeam[] = []; - const nowish = this.nowService.nowToMsEpoch(); - for (const team of this.results.teams.items) { - if (this.selectedTeamIds.length && this.selectedTeamIds.indexOf(team.id) < 0) - continue; - - if (team.session.end && team.session.end < nowish) - invalidTeamNames.push(team.name); - else - validTeamIds.push(team.id); + for (const team of teams) { + if (team.session.end && team.session.end < nowish) { + ineligibleTeams.push(team); + } + else { + eligibleTeams.push(team); + } } - if (!validTeamIds.length) { + if (ineligibleTeams.length) { this.modalService.openConfirm({ - bodyContent: "All selected teams have finished their sessions, so no resources can be deployed for them.", - hideCancel: true, - title: "All teams finished", - subtitle: this.game?.name + title: "Ineligible teams", + subtitle: this.game?.name, + bodyContent: "Some teams are ineligible to have resources deployed because they've already finished their sessions. Unselect them to proceed.\n\n" + ineligibleTeams + .map(t => ` - ${t.name || "_(no name)_"}`) + .join('\n\n'), + renderBodyAsMarkdown: true }); return; } - let appendInvalidTeamsClause = ""; - if (invalidTeamNames.length) { - appendInvalidTeamsClause = `\n\nSessions for some teams have ended, so their resources won't be deployed:\n\n${invalidTeamNames.map(tId => `- ${tId}\n`)}`; - } + await this.handleDeployGameResources(); + } + + private async handleDeployGameResources() { + const teams = this.resolveSelectedTeams(); + + // let appendInvalidTeamsClause = ""; + // if (invalidTeamNames.length) { + // appendInvalidTeamsClause = `\n\nSessions for some teams have ended, so their resources won't be deployed:\n\n${invalidTeamNames.map(tId => `- ${tId}\n`)}`; + // } this.modalService.openConfirm({ - bodyContent: `Are you sure you want to deploy resources for ${validTeamIds.length} teams?${appendInvalidTeamsClause}`, + // bodyContent: `Are you sure you want to deploy resources for ${validTeamIds.length} teams?${appendInvalidTeamsClause}`, + bodyContent: `Are you sure you want to deploy resources for ${teams.length} teams?`, onConfirm: async () => { - if (!validTeamIds.length) - return; - - await this.gameService.deployResources(this.gameId!, validTeamIds); - this.toastService.showMessage(`Deploying resources for **${validTeamIds.length} ${this.game?.isTeamGame ? "team" : "player"}(s)**.`); + await this.gameService.deployResources(this.gameId!, teams.map(t => t.id)); + this.toastService.showMessage(`Deploying resources for **${teams.length} ${this.game?.isTeamGame ? "team" : "player"}(s)**.`); this.selectedTeamIds = []; }, renderBodyAsMarkdown: true, @@ -268,6 +280,7 @@ export class GameCenterTeamsComponent implements OnInit { return; gameId = this.game?.id; + this.gameId = gameId; this.isLoading = true; this.results = await this.adminService.getGameCenterTeams(gameId!, this.filterSettings); @@ -275,6 +288,14 @@ export class GameCenterTeamsComponent implements OnInit { this.updateFilterConfig(); } + private resolveSelectedTeams() { + if (!this.results) + return []; + + const hasSelection = this.selectedTeamIds.length; + return this.results.teams.items.filter(t => !hasSelection || this.selectedTeamIds.indexOf(t.id) >= 0); + } + private updateFilterConfig() { this.localStorageClient.add(StorageKey.GameCenterTeamsFilterSettings, this.filterSettings); } diff --git a/projects/gameboard-ui/src/app/core/components/gameboard-performance-summary/gameboard-performance-summary.component.ts b/projects/gameboard-ui/src/app/core/components/gameboard-performance-summary/gameboard-performance-summary.component.ts index e0a7f636..1bb478ca 100644 --- a/projects/gameboard-ui/src/app/core/components/gameboard-performance-summary/gameboard-performance-summary.component.ts +++ b/projects/gameboard-ui/src/app/core/components/gameboard-performance-summary/gameboard-performance-summary.component.ts @@ -1,11 +1,13 @@ -import { Component, EventEmitter, Input, OnChanges, OnInit, Output, SimpleChanges } from '@angular/core'; -import { BehaviorSubject, combineLatest, Observable, of, timer } from 'rxjs'; +import { Component, Input, OnChanges, OnInit, SimpleChanges } from '@angular/core'; +import { BehaviorSubject, combineLatest, firstValueFrom, Observable, of, timer } from 'rxjs'; import { map, tap } from 'rxjs/operators'; import { calculateCountdown, TimeWindow } from '../../../api/player-models'; import { fa } from '@/services/font-awesome.service'; import { HubState, NotificationService } from '../../../services/notification.service'; +import { ChallengesService } from '@/api/challenges.service'; +import { BoardService } from '@/api/board.service'; -export interface GameboardPerformanceSummaryViewModel { +interface GameboardPerformanceSummaryViewModel { player: { id: string; teamId: string; @@ -25,28 +27,55 @@ export interface GameboardPerformanceSummaryViewModel { styleUrls: ['./gameboard-performance-summary.component.scss'] }) export class GameboardPerformanceSummaryComponent implements OnInit, OnChanges { - @Input() ctx?: GameboardPerformanceSummaryViewModel; - @Output() onRefreshRequest = new EventEmitter(); + @Input() playerId?: string; countdown$?: Observable; + protected ctx?: GameboardPerformanceSummaryViewModel; isCountdownOver = false; hubState$: BehaviorSubject; protected fa = fa; constructor( - hubService: NotificationService) { + challengesService: ChallengesService, + hubService: NotificationService, + private boardService: BoardService) { + challengesService.challengeGraded$.subscribe(async c => { + if (this.ctx && c.teamId === this.ctx.player.teamId) { + await this.load(); + } + }); this.hubState$ = hubService.state$; } ngOnInit(): void { - this.updateCountdown(); + this.load(); } ngOnChanges(changes: SimpleChanges): void { - this.updateCountdown(); + this.load(); } - private updateCountdown() { + private async load() { + if (!this.playerId) + return; + + const boardInfo = await firstValueFrom(this.boardService.load(this.playerId)); + this.ctx = { + player: { + id: this.playerId, + session: boardInfo.session, + teamId: boardInfo.teamId, + scoring: { + partialCount: boardInfo.partialCount, + correctCount: boardInfo.correctCount, + rank: boardInfo.rank, + score: boardInfo.score, + + } + } + }; + + // update the countdown this.countdown$ = combineLatest([ timer(0, 1000), of(this.ctx) diff --git a/projects/gameboard-ui/src/app/event-horizon/components/team-event-horizon/team-event-horizon.component.html b/projects/gameboard-ui/src/app/event-horizon/components/team-event-horizon/team-event-horizon.component.html index 739a00b1..292123e6 100644 --- a/projects/gameboard-ui/src/app/event-horizon/components/team-event-horizon/team-event-horizon.component.html +++ b/projects/gameboard-ui/src/app/event-horizon/components/team-event-horizon/team-event-horizon.component.html @@ -1,6 +1,9 @@
-
Click any timeline event to copy to your clipboard as markdown
+
+ Pro tip: + Click any timeline event to copy to your clipboard as markdown +
diff --git a/projects/gameboard-ui/src/app/game/gamespace-quiz/gamespace-quiz.component.ts b/projects/gameboard-ui/src/app/game/gamespace-quiz/gamespace-quiz.component.ts index d7ff4d98..42db89b1 100644 --- a/projects/gameboard-ui/src/app/game/gamespace-quiz/gamespace-quiz.component.ts +++ b/projects/gameboard-ui/src/app/game/gamespace-quiz/gamespace-quiz.component.ts @@ -122,7 +122,7 @@ export class GamespaceQuizComponent implements OnInit, OnChanges { } - // if the teamID changed, managed the team hub + // if the teamID changed, manage the team hub if (changes?.spec?.previousValue?.instance?.teamId !== this.spec?.instance?.teamId) { // (manage the team hub subscription separately to avoid orphaning subscriptions when the teamid changes) this._teamHubEventsSubscription?.unsubscribe(); diff --git a/projects/gameboard-ui/src/app/game/pages/gameboard-page/gameboard-page.component.html b/projects/gameboard-ui/src/app/game/pages/gameboard-page/gameboard-page.component.html index 9018d821..87a816e3 100644 --- a/projects/gameboard-ui/src/app/game/pages/gameboard-page/gameboard-page.component.html +++ b/projects/gameboard-ui/src/app/game/pages/gameboard-page/gameboard-page.component.html @@ -11,8 +11,7 @@
- +
diff --git a/projects/gameboard-ui/src/app/game/pages/gameboard-page/gameboard-page.component.ts b/projects/gameboard-ui/src/app/game/pages/gameboard-page/gameboard-page.component.ts index 3f831807..a2258545 100644 --- a/projects/gameboard-ui/src/app/game/pages/gameboard-page/gameboard-page.component.ts +++ b/projects/gameboard-ui/src/app/game/pages/gameboard-page/gameboard-page.component.ts @@ -14,7 +14,6 @@ import { ApiUser } from '@/api/user-models'; import { ConfigService } from '@/utility/config.service'; import { NotificationService } from '@/services/notification.service'; import { UserService } from '@/utility/user.service'; -import { GameboardPerformanceSummaryViewModel } from '@/core/components/gameboard-performance-summary/gameboard-performance-summary.component'; import { BrowserService } from '@/services/browser.service'; import { HttpErrorResponse } from '@angular/common/http'; import { ApiError } from '@/api/models'; @@ -47,17 +46,16 @@ export class GameboardPageComponent { variant = 0; user$: Observable; cid = ''; - performanceSummaryViewModel?: GameboardPerformanceSummaryViewModel; @ViewChild("startChallengeConfirmButton") protected startChallengeConfirmButton?: ConfirmButtonComponent; constructor( + challengeService: ChallengesService, route: ActivatedRoute, title: Title, usersvc: UserService, private browserService: BrowserService, private api: BoardService, - private challengeService: ChallengesService, private config: ConfigService, private hub: NotificationService, private unsub: UnsubscriberService @@ -81,20 +79,6 @@ export class GameboardPageComponent { tap(b => { this.ctx = b; title.setTitle(`${b.game.name} | ${this.config.appName}`); - - this.performanceSummaryViewModel = { - player: { - id: b.id, - teamId: b.teamId, - session: b.session, - scoring: { - rank: b.rank, - score: b.score, - partialCount: b.partialCount, - correctCount: b.correctCount - } - } - }; }), tap(b => this.startHub(b)), tap(() => this.reselect()) diff --git a/projects/gameboard-ui/src/app/game/player-session/player-session.component.html b/projects/gameboard-ui/src/app/game/player-session/player-session.component.html index b2d25042..96e0eb70 100644 --- a/projects/gameboard-ui/src/app/game/player-session/player-session.component.html +++ b/projects/gameboard-ui/src/app/game/player-session/player-session.component.html @@ -45,8 +45,8 @@ [isSyncStartGame]="ctx.game.requireSynchronizedStart"> - +
diff --git a/projects/gameboard-ui/src/app/game/player-session/player-session.component.ts b/projects/gameboard-ui/src/app/game/player-session/player-session.component.ts index 39120f21..6c2ac07c 100644 --- a/projects/gameboard-ui/src/app/game/player-session/player-session.component.ts +++ b/projects/gameboard-ui/src/app/game/player-session/player-session.component.ts @@ -10,7 +10,6 @@ import { PlayerService } from '../../api/player.service'; import { UserService } from '@/api/user.service'; import { UserService as LocalUserService } from "@/utility/user.service"; import { fa } from '@/services/font-awesome.service'; -import { GameboardPerformanceSummaryViewModel } from '../../core/components/gameboard-performance-summary/gameboard-performance-summary.component'; import { ModalConfirmConfig } from '@/core/components/modal/modal.models'; import { ModalConfirmService } from '@/services/modal-confirm.service'; import { TeamService } from '@/api/team.service'; @@ -36,12 +35,10 @@ export class PlayerSessionComponent implements OnDestroy { // sets up the modal if it's a team game that needs confirmation protected modalConfig?: ModalConfirmConfig; protected isDoubleChecking = false; - protected performanceSummaryViewModel$ = new BehaviorSubject(undefined); protected canAdminStart = false; protected canIgnoreSessionResetSettings$ = this.localUserService.can$("Play_IgnoreSessionResetSettings"); protected hasTimeRemaining = false; - protected performanceSummaryViewModel?: GameboardPerformanceSummaryViewModel; protected timeRemainingMs$?: Observable; constructor( @@ -55,25 +52,6 @@ export class PlayerSessionComponent implements OnDestroy { async ngOnInit() { this.ctxSub = this.ctx$.pipe( tap(ctx => { - let vm: GameboardPerformanceSummaryViewModel | undefined = undefined; - - if (ctx) { - vm = { - player: { - id: ctx.player.id, - teamId: ctx.player.teamId, - session: ctx.player.session, - scoring: { - rank: ctx.player.rank, - score: ctx.player.score, - correctCount: ctx.player.correctCount, - partialCount: ctx.player.partialCount - } - } - }; - } - - this.performanceSummaryViewModel$.next(vm); this.player$.next(ctx?.player); }), tap(ctx => { diff --git a/projects/gameboard-ui/src/app/reports/components/report-stat-summary/report-stat-summary.component.html b/projects/gameboard-ui/src/app/reports/components/report-stat-summary/report-stat-summary.component.html index b70c83c9..97bbafb1 100644 --- a/projects/gameboard-ui/src/app/reports/components/report-stat-summary/report-stat-summary.component.html +++ b/projects/gameboard-ui/src/app/reports/components/report-stat-summary/report-stat-summary.component.html @@ -13,7 +13,7 @@

Summary

-
    +

      {{ stat.value }}

      {{ stat.label }}

      diff --git a/projects/gameboard-ui/src/app/reports/components/reports/enrollment-report/enrollment-report.component.ts b/projects/gameboard-ui/src/app/reports/components/reports/enrollment-report/enrollment-report.component.ts index 3edb51cb..5cec5331 100644 --- a/projects/gameboard-ui/src/app/reports/components/reports/enrollment-report/enrollment-report.component.ts +++ b/projects/gameboard-ui/src/app/reports/components/reports/enrollment-report/enrollment-report.component.ts @@ -129,7 +129,9 @@ export class EnrollmentReportComponent extends ReportComponentBase !!e) .map(e => e! as ReportSummaryStat); diff --git a/projects/gameboard-ui/src/app/reports/components/reports/enrollment-report/enrollment-report.models.ts b/projects/gameboard-ui/src/app/reports/components/reports/enrollment-report/enrollment-report.models.ts index 8c4d4177..420dcd83 100644 --- a/projects/gameboard-ui/src/app/reports/components/reports/enrollment-report/enrollment-report.models.ts +++ b/projects/gameboard-ui/src/app/reports/components/reports/enrollment-report/enrollment-report.models.ts @@ -82,6 +82,8 @@ export interface EnrollmentReportStatSummary { sponsor: ReportSponsor, distinctPlayerCount: number; } + teamsWithNoSessionCount: number; + teamsWithNoStartedChallengeCount: number; } export interface EnrollmentReportLineChartGroup { diff --git a/projects/gameboard-ui/src/app/services/event-horizon-rendering.service.ts b/projects/gameboard-ui/src/app/services/event-horizon-rendering.service.ts index d0267d06..badf5497 100644 --- a/projects/gameboard-ui/src/app/services/event-horizon-rendering.service.ts +++ b/projects/gameboard-ui/src/app/services/event-horizon-rendering.service.ts @@ -71,7 +71,7 @@ export class EventHorizonRenderingService { return `
      ${groupData.content}
      `; } - public toModalHtmlContent(timelineEvent: EventHorizonGenericEvent, challengeSpec: EventHorizonChallengeSpec): string { + public toModalHtmlContent(timelineEvent: EventHorizonGenericEvent, challengeSpec: EventHorizonChallengeSpec, includeClipboardPrompt?: boolean): string { if (!timelineEvent) return ""; @@ -80,32 +80,37 @@ export class EventHorizonRenderingService { switch (timelineEvent.type) { case "challengeStarted": - detail = this.toChallengeStartedModalContent(timelineEvent, challengeSpec); + detail = this.toChallengeStartedMarkdown(timelineEvent, challengeSpec); break; case "solveComplete": - detail = this.toSolveCompleteModalContent(timelineEvent as EventHorizonSolveCompleteEvent, challengeSpec); + detail = this.toSolveCompleteMarkdown(timelineEvent as EventHorizonSolveCompleteEvent, challengeSpec); break; case "submissionScored": - detail = this.toSubmissionScoredModalContent(timelineEvent as EventHorizonSubmissionScoredEvent, challengeSpec); + detail = this.toSubmissionScoredMarkdown(timelineEvent as EventHorizonSubmissionScoredEvent, challengeSpec); break; } + let retVal = header; + if (detail) { - return `${header}\n\n${detail}\n\n_Click to copy this event to your clipboard as markdown_`; + retVal = `${retVal}\n\n${detail}`; + + if (includeClipboardPrompt) + retVal = `${retVal}\n\n_Click to copy this event to your clipboard as markdown_`; } - return ""; + return retVal; } private getTooltipHeader(timelineEvent: EventHorizonGenericEvent) { return `#### ${this.toFriendlyName(timelineEvent.type)}\n##### ${timelineEvent.timestamp.toLocaleString(DateTime.DATETIME_MED)}`; } - private toChallengeStartedModalContent(timelineEvent: EventHorizonGenericEvent, challengeSpec: EventHorizonChallengeSpec) { + private toChallengeStartedMarkdown(timelineEvent: EventHorizonGenericEvent, challengeSpec: EventHorizonChallengeSpec) { return `${challengeSpec.name} began.`; } - private toSubmissionScoredModalContent(timelineEvent: EventHorizonSubmissionScoredEvent, challengeSpec: EventHorizonChallengeSpec) { + private toSubmissionScoredMarkdown(timelineEvent: EventHorizonSubmissionScoredEvent, challengeSpec: EventHorizonChallengeSpec) { let attemptSummary = `${timelineEvent.eventData.attemptNumber}`; if (challengeSpec.maxAttempts) attemptSummary = `${attemptSummary}/${challengeSpec.maxAttempts}`; @@ -119,7 +124,7 @@ export class EventHorizonRenderingService { `.trim(); } - private toSolveCompleteModalContent(timelineEvent: EventHorizonSolveCompleteEvent, challengeSpec: EventHorizonChallengeSpec) { + private toSolveCompleteMarkdown(timelineEvent: EventHorizonSolveCompleteEvent, challengeSpec: EventHorizonChallengeSpec) { let attemptSummary = `${timelineEvent.eventData.attemptsUsed}`; if (challengeSpec.maxAttempts) attemptSummary = `${attemptSummary}/${challengeSpec.maxAttempts}`; @@ -138,7 +143,7 @@ export class EventHorizonRenderingService { content: eventName, className: `eh-event ${isClickable ? "eh-event-clickable" : ""} ${className}`, isClickable, - title: this.markdownHelpers.toHtml(this.toModalHtmlContent(timelineEvent, challengeSpec)), + title: this.markdownHelpers.toHtml(this.toModalHtmlContent(timelineEvent, challengeSpec, true)), eventData: null }; }