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 { LeagueConfigFormViewModel } from '@gridpilot/racing/application/presenters/ILeagueFullConfigPresenter'; import { LeagueFullConfigPresenter } from '@/lib/presenters/LeagueFullConfigPresenter'; import type { MembershipRole } from '@/lib/leagueMembership'; import { EntityMappers } from '@gridpilot/racing/application/mappers/EntityMappers'; import { getLeagueMembershipRepository, getDriverRepository, getGetLeagueFullConfigUseCase, getRaceRepository, getProtestRepository, getDriverStats, getAllDriverRankings, getListSeasonsForLeagueUseCase, } 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 LeagueSummaryViewModel { id: string; ownerId: string; settings: { pointsSystem: string; }; } export interface LeagueAdminProtestsViewModel { protests: Protest[]; racesById: ProtestRaceSummary; driversById: ProtestDriverSummary; } export interface LeagueAdminConfigViewModel { form: LeagueConfigFormModel | null; } export interface LeagueAdminPermissionsViewModel { canRemoveMember: boolean; canUpdateRoles: boolean; } export interface LeagueSeasonSummaryViewModel { seasonId: string; name: string; status: string; startDate?: Date; endDate?: Date; isPrimary: boolean; isParallelActive: 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) => { const base: LeagueJoinRequestViewModel = { id: request.id, leagueId: request.leagueId, driverId: request.driverId, requestedAt: request.requestedAt, }; const message = request.message; const driver = driversById[request.driverId]; return { ...base, ...(typeof message === 'string' && message.length > 0 ? { message } : {}), ...(driver ? { driver } : {}), }; }); } /** * 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({ id: request.id, 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(params: { ownerId: string; }): Promise { const driverRepo = getDriverRepository(); const entity = await driverRepo.findById(params.ownerId); if (!entity) return null; const ownerDriver = EntityMappers.toDriverDTO(entity); if (!ownerDriver) { return null; } 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 presenter = new LeagueFullConfigPresenter(); await useCase.execute({ leagueId }, presenter); const fullConfig = presenter.getViewModel(); if (!fullConfig) { return { form: null }; } const formModel: LeagueConfigFormModel = { leagueId: fullConfig.leagueId, basics: { ...fullConfig.basics, visibility: fullConfig.basics.visibility as LeagueConfigFormModel['basics']['visibility'], }, structure: { ...fullConfig.structure, mode: fullConfig.structure.mode as LeagueConfigFormModel['structure']['mode'], }, championships: fullConfig.championships, scoring: fullConfig.scoring, dropPolicy: { strategy: fullConfig.dropPolicy.strategy as LeagueConfigFormModel['dropPolicy']['strategy'], ...(fullConfig.dropPolicy.n !== undefined ? { n: fullConfig.dropPolicy.n } : {}), }, timings: fullConfig.timings, stewarding: { decisionMode: fullConfig.stewarding.decisionMode as LeagueConfigFormModel['stewarding']['decisionMode'], ...(fullConfig.stewarding.requiredVotes !== undefined ? { requiredVotes: fullConfig.stewarding.requiredVotes } : {}), requireDefense: fullConfig.stewarding.requireDefense, defenseTimeLimit: fullConfig.stewarding.defenseTimeLimit, voteTimeLimit: fullConfig.stewarding.voteTimeLimit, protestDeadlineHours: fullConfig.stewarding.protestDeadlineHours, stewardingClosesHours: fullConfig.stewarding.stewardingClosesHours, notifyAccusedOnProtest: fullConfig.stewarding.notifyAccusedOnProtest, notifyOnVoteRequired: fullConfig.stewarding.notifyOnVoteRequired, }, }; return { form: formModel }; } /** * 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, }; } export async function loadLeagueSeasons(leagueId: string): Promise { const useCase = getListSeasonsForLeagueUseCase(); const result = await useCase.execute({ leagueId }); const activeCount = result.items.filter((s) => s.status === 'active').length; return result.items.map((s) => ({ seasonId: s.seasonId, name: s.name, status: s.status, ...(s.startDate ? { startDate: s.startDate } : {}), ...(s.endDate ? { endDate: s.endDate } : {}), isPrimary: s.isPrimary ?? false, isParallelActive: activeCount > 1 && s.status === 'active', })); }