/** * Get Dashboard Use Case * * Orchestrates the retrieval of dashboard data for a driver. * Aggregates data from multiple repositories and returns a unified dashboard view. */ import { DashboardRepository } from '../ports/DashboardRepository'; import { DashboardQuery } from '../ports/DashboardQuery'; import { DashboardDTO } from '../dto/DashboardDTO'; import { DashboardEventPublisher } from '../ports/DashboardEventPublisher'; import { DriverNotFoundError } from '../../domain/errors/DriverNotFoundError'; import { ValidationError } from '../../../shared/errors/ValidationError'; export interface GetDashboardUseCasePorts { driverRepository: DashboardRepository; raceRepository: DashboardRepository; leagueRepository: DashboardRepository; activityRepository: DashboardRepository; eventPublisher: DashboardEventPublisher; } export class GetDashboardUseCase { constructor(private readonly ports: GetDashboardUseCasePorts) {} async execute(query: DashboardQuery): Promise { // Validate input this.validateQuery(query); // Find driver const driver = await this.ports.driverRepository.findDriverById(query.driverId); if (!driver) { throw new DriverNotFoundError(query.driverId); } // Fetch all data in parallel const [upcomingRaces, leagueStandings, recentActivity] = await Promise.all([ this.ports.raceRepository.getUpcomingRaces(query.driverId), this.ports.leagueRepository.getLeagueStandings(query.driverId), this.ports.activityRepository.getRecentActivity(query.driverId), ]); // Limit upcoming races to 3 const limitedRaces = upcomingRaces .sort((a, b) => a.scheduledDate.getTime() - b.scheduledDate.getTime()) .slice(0, 3); // Sort recent activity by timestamp (newest first) const sortedActivity = recentActivity .sort((a, b) => b.timestamp.getTime() - a.timestamp.getTime()); // Transform to DTO const driverDto: DashboardDTO['driver'] = { id: driver.id, name: driver.name, }; if (driver.avatar) { driverDto.avatar = driver.avatar; } const result: DashboardDTO = { driver: driverDto, statistics: { rating: driver.rating, rank: driver.rank, starts: driver.starts, wins: driver.wins, podiums: driver.podiums, leagues: driver.leagues, }, upcomingRaces: limitedRaces.map(race => ({ trackName: race.trackName, carType: race.carType, scheduledDate: race.scheduledDate.toISOString(), timeUntilRace: race.timeUntilRace || this.calculateTimeUntilRace(race.scheduledDate), })), championshipStandings: leagueStandings.map(standing => ({ leagueName: standing.leagueName, position: standing.position, points: standing.points, totalDrivers: standing.totalDrivers, })), recentActivity: sortedActivity.map(activity => ({ type: activity.type, description: activity.description, timestamp: activity.timestamp.toISOString(), status: activity.status, })), }; // Publish event await this.ports.eventPublisher.publishDashboardAccessed({ type: 'dashboard_accessed', driverId: query.driverId, timestamp: new Date(), }); return result; } private validateQuery(query: DashboardQuery): void { if (!query.driverId || typeof query.driverId !== 'string') { throw new ValidationError('Driver ID must be a valid string'); } if (query.driverId.trim().length === 0) { throw new ValidationError('Driver ID cannot be empty'); } } private calculateTimeUntilRace(scheduledDate: Date): string { const now = new Date(); const diff = scheduledDate.getTime() - now.getTime(); if (diff <= 0) { return 'Race started'; } const days = Math.floor(diff / (1000 * 60 * 60 * 24)); const hours = Math.floor((diff % (1000 * 60 * 60 * 24)) / (1000 * 60 * 60)); const minutes = Math.floor((diff % (1000 * 60 * 60)) / (1000 * 60)); if (days > 0) { return `${days} day${days > 1 ? 's' : ''} ${hours} hour${hours > 1 ? 's' : ''}`; } if (hours > 0) { return `${hours} hour${hours > 1 ? 's' : ''} ${minutes} minute${minutes > 1 ? 's' : ''}`; } return `${minutes} minute${minutes > 1 ? 's' : ''}`; } }