Files
gridpilot.gg/apps/website/lib/presenters/LeagueAdminPresenter.ts
2025-12-11 00:57:32 +01:00

292 lines
8.5 KiB
TypeScript

import type { League } from '@gridpilot/racing/domain/entities/League';
import type { Protest } from '@gridpilot/racing/domain/entities/Protest';
import type { Race } from '@gridpilot/racing/domain/entities/Race';
import type { DriverDTO } from '@gridpilot/racing/application/dto/DriverDTO';
import type { LeagueConfigFormModel } from '@gridpilot/racing/application';
import type { MembershipRole } from '@/lib/leagueMembership';
import { EntityMappers } from '@gridpilot/racing/application/mappers/EntityMappers';
import {
getLeagueMembershipRepository,
getDriverRepository,
getGetLeagueFullConfigUseCase,
getRaceRepository,
getProtestRepository,
getDriverStats,
getAllDriverRankings,
} from '@/lib/di-container';
export interface LeagueJoinRequestViewModel {
id: string;
leagueId: string;
driverId: string;
requestedAt: Date;
message?: string;
driver?: DriverDTO;
}
export interface ProtestDriverSummary {
[driverId: string]: DriverDTO;
}
export interface ProtestRaceSummary {
[raceId: string]: Race;
}
export interface LeagueOwnerSummaryViewModel {
driver: DriverDTO;
rating: number | null;
rank: number | null;
}
export interface LeagueAdminProtestsViewModel {
protests: Protest[];
racesById: ProtestRaceSummary;
driversById: ProtestDriverSummary;
}
export interface LeagueAdminConfigViewModel {
form: LeagueConfigFormModel | null;
}
export interface LeagueAdminPermissionsViewModel {
canRemoveMember: boolean;
canUpdateRoles: boolean;
}
export interface LeagueAdminViewModel {
joinRequests: LeagueJoinRequestViewModel[];
ownerSummary: LeagueOwnerSummaryViewModel | null;
config: LeagueAdminConfigViewModel;
protests: LeagueAdminProtestsViewModel;
}
/**
* Load join requests plus requester driver DTOs for a league.
*/
export async function loadLeagueJoinRequests(leagueId: string): Promise<LeagueJoinRequestViewModel[]> {
const membershipRepo = getLeagueMembershipRepository();
const requests = await membershipRepo.getJoinRequests(leagueId);
const driverRepo = getDriverRepository();
const uniqueDriverIds = Array.from(new Set(requests.map((r) => r.driverId)));
const driverEntities = await Promise.all(uniqueDriverIds.map((id) => driverRepo.findById(id)));
const driverDtos = driverEntities
.map((driver) => (driver ? EntityMappers.toDriverDTO(driver) : null))
.filter((dto): dto is DriverDTO => dto !== null);
const driversById: Record<string, DriverDTO> = {};
for (const dto of driverDtos) {
driversById[dto.id] = dto;
}
return requests.map((request) => ({
id: request.id,
leagueId: request.leagueId,
driverId: request.driverId,
requestedAt: request.requestedAt,
message: request.message,
driver: driversById[request.driverId],
}));
}
/**
* Approve a league join request and return updated join requests.
*/
export async function approveLeagueJoinRequest(
leagueId: string,
requestId: string
): Promise<LeagueJoinRequestViewModel[]> {
const membershipRepo = getLeagueMembershipRepository();
const requests = await membershipRepo.getJoinRequests(leagueId);
const request = requests.find((r) => r.id === requestId);
if (!request) {
throw new Error('Join request not found');
}
await membershipRepo.saveMembership({
leagueId: request.leagueId,
driverId: request.driverId,
role: 'member',
status: 'active',
joinedAt: new Date(),
});
await membershipRepo.removeJoinRequest(requestId);
return loadLeagueJoinRequests(leagueId);
}
/**
* Reject a league join request (alpha: just remove).
*/
export async function rejectLeagueJoinRequest(
leagueId: string,
requestId: string
): Promise<LeagueJoinRequestViewModel[]> {
const membershipRepo = getLeagueMembershipRepository();
await membershipRepo.removeJoinRequest(requestId);
return loadLeagueJoinRequests(leagueId);
}
/**
* Compute permissions for a performer on league membership actions.
*/
export async function getLeagueAdminPermissions(
leagueId: string,
performerDriverId: string
): Promise<LeagueAdminPermissionsViewModel> {
const membershipRepo = getLeagueMembershipRepository();
const performer = await membershipRepo.getMembership(leagueId, performerDriverId);
const isOwner = performer?.role === 'owner';
const isAdmin = performer?.role === 'admin';
return {
canRemoveMember: Boolean(isOwner || isAdmin),
canUpdateRoles: Boolean(isOwner),
};
}
/**
* Remove a member from the league, enforcing simple role rules.
*/
export async function removeLeagueMember(
leagueId: string,
performerDriverId: string,
targetDriverId: string
): Promise<void> {
const membershipRepo = getLeagueMembershipRepository();
const performer = await membershipRepo.getMembership(leagueId, performerDriverId);
if (!performer || (performer.role !== 'owner' && performer.role !== 'admin')) {
throw new Error('Only owners or admins can remove members');
}
const membership = await membershipRepo.getMembership(leagueId, targetDriverId);
if (!membership) {
throw new Error('Member not found');
}
if (membership.role === 'owner') {
throw new Error('Cannot remove the league owner');
}
await membershipRepo.removeMembership(leagueId, targetDriverId);
}
/**
* Update a member's role, enforcing simple owner-only rules.
*/
export async function updateLeagueMemberRole(
leagueId: string,
performerDriverId: string,
targetDriverId: string,
newRole: MembershipRole
): Promise<void> {
const membershipRepo = getLeagueMembershipRepository();
const performer = await membershipRepo.getMembership(leagueId, performerDriverId);
if (!performer || performer.role !== 'owner') {
throw new Error('Only the league owner can update roles');
}
const membership = await membershipRepo.getMembership(leagueId, targetDriverId);
if (!membership) {
throw new Error('Member not found');
}
if (membership.role === 'owner') {
throw new Error('Cannot change the owner role');
}
await membershipRepo.saveMembership({
...membership,
role: newRole,
});
}
/**
* Load owner summary (DTO + rating/rank) for a league.
*/
export async function loadLeagueOwnerSummary(league: League): Promise<LeagueOwnerSummaryViewModel | null> {
const driverRepo = getDriverRepository();
const entity = await driverRepo.findById(league.ownerId);
if (!entity) return null;
const ownerDriver = EntityMappers.toDriverDTO(entity);
const stats = getDriverStats(ownerDriver.id);
const allRankings = getAllDriverRankings();
let rating: number | null = stats?.rating ?? null;
let rank: number | null = null;
if (stats) {
if (typeof stats.overallRank === 'number' && stats.overallRank > 0) {
rank = stats.overallRank;
} else {
const indexInGlobal = allRankings.findIndex((stat) => stat.driverId === stats.driverId);
if (indexInGlobal !== -1) {
rank = indexInGlobal + 1;
}
}
if (rating === null) {
const globalEntry = allRankings.find((stat) => stat.driverId === stats.driverId);
if (globalEntry) {
rating = globalEntry.rating;
}
}
}
return {
driver: ownerDriver,
rating,
rank,
};
}
/**
* Load league full config form.
*/
export async function loadLeagueConfig(leagueId: string): Promise<LeagueAdminConfigViewModel> {
const useCase = getGetLeagueFullConfigUseCase();
const form = await useCase.execute({ leagueId });
return { form };
}
/**
* Load protests, related races and driver DTOs for a league.
*/
export async function loadLeagueProtests(leagueId: string): Promise<LeagueAdminProtestsViewModel> {
const raceRepo = getRaceRepository();
const protestRepo = getProtestRepository();
const driverRepo = getDriverRepository();
const leagueRaces = await raceRepo.findByLeagueId(leagueId);
const allProtests: Protest[] = [];
const racesById: Record<string, Race> = {};
for (const race of leagueRaces) {
racesById[race.id] = race;
const raceProtests = await protestRepo.findByRaceId(race.id);
allProtests.push(...raceProtests);
}
const driverIds = new Set<string>();
allProtests.forEach((p) => {
driverIds.add(p.protestingDriverId);
driverIds.add(p.accusedDriverId);
});
const driverEntities = await Promise.all(Array.from(driverIds).map((id) => driverRepo.findById(id)));
const driverDtos = driverEntities
.map((driver) => (driver ? EntityMappers.toDriverDTO(driver) : null))
.filter((dto): dto is DriverDTO => dto !== null);
const driversById: Record<string, DriverDTO> = {};
for (const dto of driverDtos) {
driversById[dto.id] = dto;
}
return {
protests: allProtests,
racesById,
driversById,
};
}