This commit is contained in:
2025-12-11 11:25:22 +01:00
parent 6a427eab57
commit e4c1be628d
86 changed files with 1222 additions and 736 deletions

View File

@@ -43,7 +43,7 @@ import type { INotificationRepository, INotificationPreferenceRepository } from
import {
SendNotificationUseCase,
MarkNotificationReadUseCase,
GetUnreadNotificationsQuery
GetUnreadNotificationsUseCase
} from '@gridpilot/notifications/application';
import {
InMemoryNotificationRepository,
@@ -81,7 +81,7 @@ import {
InMemoryFeedRepository,
InMemorySocialGraphRepository,
} from '@gridpilot/social/infrastructure/inmemory/InMemorySocialAndFeed';
import { DemoImageServiceAdapter } from '@gridpilot/demo-infrastructure';
import { DemoImageServiceAdapter } from '@gridpilot/testing-support';
// Application use cases and queries
import {
@@ -174,7 +174,7 @@ import { LeagueSchedulePreviewPresenter } from './presenters/LeagueSchedulePrevi
import { DashboardOverviewPresenter } from './presenters/DashboardOverviewPresenter';
import { ProfileOverviewPresenter } from './presenters/ProfileOverviewPresenter';
// Testing support
// Demo infrastructure (runtime demo seed & helpers)
import {
createStaticRacingSeed,
getDemoLeagueArchetypeByName,
@@ -1246,8 +1246,8 @@ export function configureDIContainer(): void {
// Register queries - Notifications
container.registerInstance(
DI_TOKENS.GetUnreadNotificationsQuery,
new GetUnreadNotificationsQuery(notificationRepository)
DI_TOKENS.GetUnreadNotificationsUseCase,
new GetUnreadNotificationsUseCase(notificationRepository)
);
// Register use cases - Sponsors

View File

@@ -35,7 +35,7 @@ import type { INotificationRepository, INotificationPreferenceRepository } from
import type {
SendNotificationUseCase,
MarkNotificationReadUseCase,
GetUnreadNotificationsQuery
GetUnreadNotificationsUseCase
} from '@gridpilot/notifications/application';
import type {
JoinLeagueUseCase,
@@ -457,9 +457,9 @@ class DIContainer {
return getDIContainer().resolve<MarkNotificationReadUseCase>(DI_TOKENS.MarkNotificationReadUseCase);
}
get getUnreadNotificationsQuery(): GetUnreadNotificationsQuery {
get getUnreadNotificationsUseCase(): GetUnreadNotificationsUseCase {
this.ensureInitialized();
return getDIContainer().resolve<GetUnreadNotificationsQuery>(DI_TOKENS.GetUnreadNotificationsQuery);
return getDIContainer().resolve<GetUnreadNotificationsUseCase>(DI_TOKENS.GetUnreadNotificationsUseCase);
}
get fileProtestUseCase(): FileProtestUseCase {
@@ -801,8 +801,8 @@ export function getMarkNotificationReadUseCase(): MarkNotificationReadUseCase {
return DIContainer.getInstance().markNotificationReadUseCase;
}
export function getGetUnreadNotificationsQuery(): GetUnreadNotificationsQuery {
return DIContainer.getInstance().getUnreadNotificationsQuery;
export function getGetUnreadNotificationsUseCase(): GetUnreadNotificationsUseCase {
return DIContainer.getInstance().getUnreadNotificationsUseCase;
}
export function getFileProtestUseCase(): FileProtestUseCase {

View File

@@ -101,7 +101,7 @@ export const DI_TOKENS = {
GetRacePenaltiesUseCase: Symbol.for('GetRacePenaltiesUseCase'),
// Queries - Notifications
GetUnreadNotificationsQuery: Symbol.for('GetUnreadNotificationsQuery'),
GetUnreadNotificationsUseCase: Symbol.for('GetUnreadNotificationsUseCase'),
// Use Cases - Sponsors
GetSponsorDashboardUseCase: Symbol.for('GetSponsorDashboardUseCase'),

View File

@@ -0,0 +1,121 @@
import type { Team, TeamJoinRequest } from '@gridpilot/racing';
import type { DriverDTO } from '@gridpilot/racing/application/dto/DriverDTO';
import { EntityMappers } from '@gridpilot/racing/application/mappers/EntityMappers';
import {
getDriverRepository,
getGetTeamJoinRequestsUseCase,
getApproveTeamJoinRequestUseCase,
getRejectTeamJoinRequestUseCase,
getUpdateTeamUseCase,
} from '@/lib/di-container';
export interface TeamAdminJoinRequestViewModel {
id: string;
teamId: string;
driverId: string;
requestedAt: Date;
message?: string;
driver?: DriverDTO;
}
export interface TeamAdminTeamSummaryViewModel {
id: string;
name: string;
tag: string;
description: string;
ownerId: string;
}
export interface TeamAdminViewModel {
team: TeamAdminTeamSummaryViewModel;
requests: TeamAdminJoinRequestViewModel[];
}
/**
* Load join requests plus driver DTOs for a team.
*/
export async function loadTeamAdminViewModel(team: Team): Promise<TeamAdminViewModel> {
const requests = await loadTeamJoinRequests(team.id);
return {
team: {
id: team.id,
name: team.name,
tag: team.tag,
description: team.description,
ownerId: team.ownerId,
},
requests,
};
}
export async function loadTeamJoinRequests(teamId: string): Promise<TeamAdminJoinRequestViewModel[]> {
const getRequestsUseCase = getGetTeamJoinRequestsUseCase();
await getRequestsUseCase.execute({ teamId });
const presenterVm = getRequestsUseCase.presenter.getViewModel();
const driverRepo = getDriverRepository();
const allDrivers = await driverRepo.findAll();
const driversById: Record<string, DriverDTO> = {};
for (const driver of allDrivers) {
const dto = EntityMappers.toDriverDTO(driver);
if (dto) {
driversById[dto.id] = dto;
}
}
return presenterVm.requests.map((req) => ({
id: req.requestId,
teamId: req.teamId,
driverId: req.driverId,
requestedAt: new Date(req.requestedAt),
message: req.message,
driver: driversById[req.driverId],
}));
}
/**
* Approve a team join request and return updated request view models.
*/
export async function approveTeamJoinRequestAndReload(
requestId: string,
teamId: string,
): Promise<TeamAdminJoinRequestViewModel[]> {
const useCase = getApproveTeamJoinRequestUseCase();
await useCase.execute({ requestId });
return loadTeamJoinRequests(teamId);
}
/**
* Reject a team join request and return updated request view models.
*/
export async function rejectTeamJoinRequestAndReload(
requestId: string,
teamId: string,
): Promise<TeamAdminJoinRequestViewModel[]> {
const useCase = getRejectTeamJoinRequestUseCase();
await useCase.execute({ requestId });
return loadTeamJoinRequests(teamId);
}
/**
* Update team basic details.
*/
export async function updateTeamDetails(params: {
teamId: string;
name: string;
tag: string;
description: string;
updatedByDriverId: string;
}): Promise<void> {
const useCase = getUpdateTeamUseCase();
await useCase.execute({
teamId: params.teamId,
updates: {
name: params.name,
tag: params.tag,
description: params.description,
},
updatedBy: params.updatedByDriverId,
});
}

View File

@@ -0,0 +1,76 @@
import { getStandingRepository, getLeagueRepository, getTeamMembershipRepository } from '@/lib/di-container';
export interface TeamLeagueStandingViewModel {
leagueId: string;
leagueName: string;
position: number;
points: number;
wins: number;
racesCompleted: number;
}
export interface TeamStandingsViewModel {
standings: TeamLeagueStandingViewModel[];
}
/**
* Compute team standings across the given leagues for a team.
* Mirrors the previous TeamStandings component logic but keeps it out of the UI layer.
*/
export async function loadTeamStandings(
teamId: string,
leagues: string[],
): Promise<TeamStandingsViewModel> {
const standingRepo = getStandingRepository();
const leagueRepo = getLeagueRepository();
const teamMembershipRepo = getTeamMembershipRepository();
const members = await teamMembershipRepo.getTeamMembers(teamId);
const memberIds = members.map((m) => m.driverId);
const teamStandings: TeamLeagueStandingViewModel[] = [];
for (const leagueId of leagues) {
const league = await leagueRepo.findById(leagueId);
if (!league) continue;
const leagueStandings = await standingRepo.findByLeagueId(leagueId);
let totalPoints = 0;
let totalWins = 0;
let totalRaces = 0;
for (const standing of leagueStandings) {
if (memberIds.includes(standing.driverId)) {
totalPoints += standing.points;
totalWins += standing.wins;
totalRaces = Math.max(totalRaces, standing.racesCompleted);
}
}
// Simplified team position based on total points (same spirit as previous logic)
const allTeamPoints = leagueStandings
.filter((s) => memberIds.includes(s.driverId))
.reduce((sum, s) => sum + s.points, 0);
const position =
leagueStandings
.filter((_, idx, arr) => {
const teamPoints = arr
.filter((s) => memberIds.includes(s.driverId))
.reduce((sum, s) => sum + s.points, 0);
return teamPoints > allTeamPoints;
}).length + 1;
teamStandings.push({
leagueId,
leagueName: league.name,
position,
points: totalPoints,
wins: totalWins,
racesCompleted: totalRaces,
});
}
return { standings: teamStandings };
}