import type { IDriverRepository } from '../../domain/repositories/IDriverRepository'; import type { IRaceRepository } from '../../domain/repositories/IRaceRepository'; import type { IResultRepository } from '../../domain/repositories/IResultRepository'; import type { ILeagueRepository } from '../../domain/repositories/ILeagueRepository'; import type { IStandingRepository } from '../../domain/repositories/IStandingRepository'; import type { ILeagueMembershipRepository } from '../../domain/repositories/ILeagueMembershipRepository'; import type { IRaceRegistrationRepository } from '../../domain/repositories/IRaceRegistrationRepository'; import type { IImageService } from '../../domain/services/IImageService'; import type { IFeedRepository } from '@gridpilot/social/domain/repositories/IFeedRepository'; import type { ISocialGraphRepository } from '@gridpilot/social/domain/repositories/ISocialGraphRepository'; import type { IDashboardOverviewPresenter, DashboardOverviewViewModel, DashboardDriverSummaryViewModel, DashboardRaceSummaryViewModel, DashboardRecentResultViewModel, DashboardLeagueStandingSummaryViewModel, DashboardFeedItemSummaryViewModel, DashboardFeedSummaryViewModel, DashboardFriendSummaryViewModel, } from '../presenters/IDashboardOverviewPresenter'; interface DashboardDriverStatsAdapter { rating: number | null; wins: number; podiums: number; totalRaces: number; overallRank: number | null; consistency: number | null; } export interface GetDashboardOverviewParams { driverId: string; } export class GetDashboardOverviewUseCase { constructor( private readonly driverRepository: IDriverRepository, private readonly raceRepository: IRaceRepository, private readonly resultRepository: IResultRepository, private readonly leagueRepository: ILeagueRepository, private readonly standingRepository: IStandingRepository, private readonly leagueMembershipRepository: ILeagueMembershipRepository, private readonly raceRegistrationRepository: IRaceRegistrationRepository, private readonly feedRepository: IFeedRepository, private readonly socialRepository: ISocialGraphRepository, private readonly imageService: IImageService, private readonly getDriverStats: (driverId: string) => DashboardDriverStatsAdapter | null, public readonly presenter: IDashboardOverviewPresenter, ) {} async execute(params: GetDashboardOverviewParams): Promise { const { driverId } = params; const [driver, allLeagues, allRaces, allResults, feedItems, friends] = await Promise.all([ this.driverRepository.findById(driverId), this.leagueRepository.findAll(), this.raceRepository.findAll(), this.resultRepository.findAll(), this.feedRepository.getFeedForDriver(driverId), this.socialRepository.getFriends(driverId), ]); const leagueMap = new Map(allLeagues.map(league => [league.id, league.name])); const driverStats = this.getDriverStats(driverId); const currentDriver: DashboardDriverSummaryViewModel | null = driver ? { id: driver.id, name: driver.name, country: driver.country, avatarUrl: this.imageService.getDriverAvatar(driver.id), rating: driverStats?.rating ?? null, globalRank: driverStats?.overallRank ?? null, totalRaces: driverStats?.totalRaces ?? 0, wins: driverStats?.wins ?? 0, podiums: driverStats?.podiums ?? 0, consistency: driverStats?.consistency ?? null, } : null; const driverLeagues = await this.getDriverLeagues(allLeagues, driverId); const driverLeagueIds = new Set(driverLeagues.map(league => league.id)); const now = new Date(); const upcomingRaces = allRaces .filter(race => race.status === 'scheduled' && race.scheduledAt > now) .sort((a, b) => a.scheduledAt.getTime() - b.scheduledAt.getTime()); const upcomingRacesInDriverLeagues = upcomingRaces.filter(race => driverLeagueIds.has(race.leagueId), ); const { myUpcomingRaces, otherUpcomingRaces } = await this.partitionUpcomingRacesByRegistration(upcomingRacesInDriverLeagues, driverId, leagueMap); const nextRace: DashboardRaceSummaryViewModel | null = myUpcomingRaces.length > 0 ? myUpcomingRaces[0]! : null; const upcomingRacesSummaries: DashboardRaceSummaryViewModel[] = [ ...myUpcomingRaces, ...otherUpcomingRaces, ].slice().sort( (a, b) => new Date(a.scheduledAt).getTime() - new Date(b.scheduledAt).getTime(), ); const recentResults = this.buildRecentResults(allResults, allRaces, allLeagues, driverId); const leagueStandingsSummaries = await this.buildLeagueStandingsSummaries( driverLeagues, driverId, ); const activeLeaguesCount = this.computeActiveLeaguesCount( upcomingRacesSummaries, leagueStandingsSummaries, ); const feedSummary = this.buildFeedSummary(feedItems); const friendsSummary = this.buildFriendsSummary(friends); const viewModel: DashboardOverviewViewModel = { currentDriver, myUpcomingRaces, otherUpcomingRaces, upcomingRaces: upcomingRacesSummaries, activeLeaguesCount, nextRace, recentResults, leagueStandingsSummaries, feedSummary, friends: friendsSummary, }; this.presenter.present(viewModel); } private async getDriverLeagues(allLeagues: any[], driverId: string): Promise { const driverLeagues: any[] = []; for (const league of allLeagues) { const membership = await this.leagueMembershipRepository.getMembership(league.id, driverId); if (membership && membership.status === 'active') { driverLeagues.push(league); } } return driverLeagues; } private async partitionUpcomingRacesByRegistration( upcomingRaces: any[], driverId: string, leagueMap: Map, ): Promise<{ myUpcomingRaces: DashboardRaceSummaryViewModel[]; otherUpcomingRaces: DashboardRaceSummaryViewModel[]; }> { const myUpcomingRaces: DashboardRaceSummaryViewModel[] = []; const otherUpcomingRaces: DashboardRaceSummaryViewModel[] = []; for (const race of upcomingRaces) { const isRegistered = await this.raceRegistrationRepository.isRegistered(race.id, driverId); const summary = this.mapRaceToSummary(race, leagueMap, true); if (isRegistered) { myUpcomingRaces.push(summary); } else { otherUpcomingRaces.push(summary); } } return { myUpcomingRaces, otherUpcomingRaces }; } private mapRaceToSummary( race: any, leagueMap: Map, isMyLeague: boolean, ): DashboardRaceSummaryViewModel { return { id: race.id, leagueId: race.leagueId, leagueName: leagueMap.get(race.leagueId) ?? 'Unknown League', track: race.track, car: race.car, scheduledAt: race.scheduledAt.toISOString(), status: race.status, isMyLeague, }; } private buildRecentResults( allResults: any[], allRaces: any[], allLeagues: any[], driverId: string, ): DashboardRecentResultViewModel[] { const raceById = new Map(allRaces.map(race => [race.id, race])); const leagueById = new Map(allLeagues.map(league => [league.id, league])); const driverResults = allResults.filter(result => result.driverId === driverId); const enriched = driverResults .map(result => { const race = raceById.get(result.raceId); if (!race) return null; const league = leagueById.get(race.leagueId); const finishedAt = race.scheduledAt.toISOString(); const item: DashboardRecentResultViewModel = { raceId: race.id, raceName: race.track, leagueId: race.leagueId, leagueName: league?.name ?? 'Unknown League', finishedAt, position: result.position, incidents: result.incidents, }; return item; }) .filter((item): item is DashboardRecentResultViewModel => !!item) .sort( (a, b) => new Date(b.finishedAt).getTime() - new Date(a.finishedAt).getTime(), ); const RECENT_RESULTS_LIMIT = 5; return enriched.slice(0, RECENT_RESULTS_LIMIT); } private async buildLeagueStandingsSummaries( driverLeagues: any[], driverId: string, ): Promise { const summaries: DashboardLeagueStandingSummaryViewModel[] = []; for (const league of driverLeagues.slice(0, 3)) { const standings = await this.standingRepository.findByLeagueId(league.id); const driverStanding = standings.find( (standing: any) => standing.driverId === driverId, ); summaries.push({ leagueId: league.id, leagueName: league.name, position: driverStanding?.position ?? 0, points: driverStanding?.points ?? 0, totalDrivers: standings.length, }); } return summaries; } private computeActiveLeaguesCount( upcomingRaces: DashboardRaceSummaryViewModel[], leagueStandingsSummaries: DashboardLeagueStandingSummaryViewModel[], ): number { const activeLeagueIds = new Set(); for (const race of upcomingRaces) { activeLeagueIds.add(race.leagueId); } for (const standing of leagueStandingsSummaries) { activeLeagueIds.add(standing.leagueId); } return activeLeagueIds.size; } private buildFeedSummary(feedItems: any[]): DashboardFeedSummaryViewModel { const items: DashboardFeedItemSummaryViewModel[] = feedItems.map(item => ({ id: item.id, type: item.type, headline: item.headline, body: item.body, timestamp: item.timestamp instanceof Date ? item.timestamp.toISOString() : new Date(item.timestamp).toISOString(), ctaLabel: item.ctaLabel, ctaHref: item.ctaHref, })); return { notificationCount: items.length, items, }; } private buildFriendsSummary(friends: any[]): DashboardFriendSummaryViewModel[] { return friends.map(friend => ({ id: friend.id, name: friend.name, country: friend.country, avatarUrl: this.imageService.getDriverAvatar(friend.id), })); } }