Skip to content

Commit

Permalink
Fix event horizon, game center teams, enrollment report stat summary,…
Browse files Browse the repository at this point in the history
… progress widget
  • Loading branch information
sei-bstein committed Oct 15, 2024
1 parent 81b35fd commit 5df6804
Show file tree
Hide file tree
Showing 13 changed files with 120 additions and 97 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@
</button>

<button type="button" class="btn btn-info" tooltip="Deploy game resources"
(click)="handleDeployGameResources()">
[disabled]="!results?.teams?.items?.length" (click)="handleConfirmDeployGameResources()">
<fa-icon [icon]="fa.computer"></fa-icon>
</button>

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down Expand Up @@ -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,
Expand Down Expand Up @@ -268,13 +280,22 @@ 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);
this.isLoading = false;
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);
}
Expand Down
Original file line number Diff line number Diff line change
@@ -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;
Expand All @@ -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<string>();
@Input() playerId?: string;

countdown$?: Observable<number | undefined>;
protected ctx?: GameboardPerformanceSummaryViewModel;
isCountdownOver = false;
hubState$: BehaviorSubject<HubState>;
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)
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,9 @@
<div #timelineContainer></div>
<div *ngIf="timelineViewModel; else noTimeline" class="team-event-horizon-component pb-3">
<div class="text-center text-muted fs-08 mt-1">Click any timeline event to copy to your clipboard as markdown</div>
<div class="text-center text-muted fs-08 mt-1">
<strong>Pro tip:</strong>
Click any timeline event to copy to your clipboard as markdown
</div>
<div class="controls-container d-flex justify-content-center align-content-center mt-3">
<div class="btn-group btn-group-toggle" role="group" aria-label="Event type selection group"
data-toggle="buttons">
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,8 +11,7 @@
</span>
<div class="spacer"></div>
<div>
<app-gameboard-performance-summary [ctx]="performanceSummaryViewModel"
(onRefreshRequest)="handleRefreshRequest($event)"></app-gameboard-performance-summary>
<app-gameboard-performance-summary [playerId]="ctx.id"></app-gameboard-performance-summary>
</div>
</div>

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down Expand Up @@ -47,17 +46,16 @@ export class GameboardPageComponent {
variant = 0;
user$: Observable<ApiUser | null>;
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
Expand All @@ -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())
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -45,8 +45,8 @@
[isSyncStartGame]="ctx.game.requireSynchronizedStart"></app-player-presence>
</ng-container>

<app-gameboard-performance-summary *ngIf="performanceSummaryViewModel"
[ctx]="performanceSummaryViewModel"></app-gameboard-performance-summary>
<app-gameboard-performance-summary *ngIf="ctx.player?.id"
[playerId]="ctx.player.id"></app-gameboard-performance-summary>

<div class="d-flex my-4 align-items-center justify-content-center">
<a type="button" class="btn btn-secondary" target="_blank" [href]="'game/scores/' + ctx.game.id">
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand All @@ -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<GameboardPerformanceSummaryViewModel | undefined>(undefined);

protected canAdminStart = false;
protected canIgnoreSessionResetSettings$ = this.localUserService.can$("Play_IgnoreSessionResetSettings");
protected hasTimeRemaining = false;
protected performanceSummaryViewModel?: GameboardPerformanceSummaryViewModel;
protected timeRemainingMs$?: Observable<number>;

constructor(
Expand All @@ -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 => {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ <h2 class="text-center text-upper mb-5">Summary</h2>
</span>
</p>
</div>
<ul class="overall-stats d-flex align-items-center justify-content-center mb-5">
<ul class="overall-stats d-flex align-items-start justify-content-center mb-5">
<div *ngFor="let stat of stats" class="stat">
<p class="value text-info" [class.small-value]="stat.valueSize == 'small'">{{ stat.value }}</p>
<p class="label">{{ stat.label }}</p>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -129,7 +129,9 @@ export class EnrollmentReportComponent extends ReportComponentBase<EnrollmentRep
{ label: "Games", value: stats.distinctGameCount },
{ label: "Players", value: stats.distinctPlayerCount },
{ label: "Teams", value: stats.distinctTeamCount },
{ label: "Sponsors", value: stats.distinctSponsorCount }
{ label: "Sponsors", value: stats.distinctSponsorCount },
{ label: "Teams With Unstarted Sessions", value: stats.teamsWithNoSessionCount },
{ label: "Teams With No Started Challenges", value: stats.teamsWithNoStartedChallengeCount }
]
.filter(e => !!e)
.map(e => e! as ReportSummaryStat);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -82,6 +82,8 @@ export interface EnrollmentReportStatSummary {
sponsor: ReportSponsor,
distinctPlayerCount: number;
}
teamsWithNoSessionCount: number;
teamsWithNoStartedChallengeCount: number;
}

export interface EnrollmentReportLineChartGroup {
Expand Down
Loading

0 comments on commit 5df6804

Please sign in to comment.