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 { 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 = {}; 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 { 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 { 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 { 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 { 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 { 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 { 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 { 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 { const raceRepo = getRaceRepository(); const protestRepo = getProtestRepository(); const driverRepo = getDriverRepository(); const leagueRaces = await raceRepo.findByLeagueId(leagueId); const allProtests: Protest[] = []; const racesById: Record = {}; for (const race of leagueRaces) { racesById[race.id] = race; const raceProtests = await protestRepo.findByRaceId(race.id); allProtests.push(...raceProtests); } const driverIds = new Set(); 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 = {}; for (const dto of driverDtos) { driversById[dto.id] = dto; } return { protests: allProtests, racesById, driversById, }; }