383 lines
11 KiB
TypeScript
383 lines
11 KiB
TypeScript
import { Result } from '@core/shared/application/Result';
|
|
import type { ApplicationErrorCode } from '@core/shared/errors/ApplicationErrorCode';
|
|
import type { UseCaseOutputPort } from '@core/shared/application/UseCaseOutputPort';
|
|
import type { IFeedRepository } from '@core/social/domain/repositories/IFeedRepository';
|
|
import type { ISocialGraphRepository } from '@core/social/domain/repositories/ISocialGraphRepository';
|
|
import type { FeedItem } from '@core/social/domain/types/FeedItem';
|
|
import { Driver } from '../../domain/entities/Driver';
|
|
import { League } from '../../domain/entities/League';
|
|
import { Race } from '../../domain/entities/Race';
|
|
import { Standing } from '../../domain/entities/Standing';
|
|
import type { IDriverRepository } from '../../domain/repositories/IDriverRepository';
|
|
import type { ILeagueMembershipRepository } from '../../domain/repositories/ILeagueMembershipRepository';
|
|
import type { ILeagueRepository } from '../../domain/repositories/ILeagueRepository';
|
|
import type { IRaceRegistrationRepository } from '../../domain/repositories/IRaceRegistrationRepository';
|
|
import type { IRaceRepository } from '../../domain/repositories/IRaceRepository';
|
|
import type { IResultRepository } from '../../domain/repositories/IResultRepository';
|
|
import type { IStandingRepository } from '../../domain/repositories/IStandingRepository';
|
|
import type { Result as RaceResult } from '../../domain/entities/result/Result';
|
|
|
|
export interface DashboardOverviewInput {
|
|
driverId: string;
|
|
}
|
|
|
|
interface DashboardDriverStatsAdapter {
|
|
rating: number | null;
|
|
wins: number;
|
|
podiums: number;
|
|
totalRaces: number;
|
|
overallRank: number | null;
|
|
consistency: number | null;
|
|
}
|
|
|
|
export interface DashboardDriverSummary {
|
|
driver: Driver;
|
|
avatarUrl: string | null;
|
|
rating: number | null;
|
|
globalRank: number | null;
|
|
totalRaces: number;
|
|
wins: number;
|
|
podiums: number;
|
|
consistency: number | null;
|
|
}
|
|
|
|
export interface DashboardRaceSummary {
|
|
race: Race;
|
|
league: League | null;
|
|
isMyLeague: boolean;
|
|
}
|
|
|
|
export interface DashboardRecentRaceResultSummary {
|
|
race: Race;
|
|
league: League | null;
|
|
result: RaceResult;
|
|
}
|
|
|
|
export interface DashboardLeagueStandingSummary {
|
|
league: League;
|
|
standing: Standing | null;
|
|
totalDrivers: number;
|
|
}
|
|
|
|
export interface DashboardFeedSummary {
|
|
notificationCount: number;
|
|
items: FeedItem[];
|
|
}
|
|
|
|
export interface DashboardFriendSummary {
|
|
driver: Driver;
|
|
avatarUrl: string | null;
|
|
}
|
|
|
|
export interface DashboardOverviewResult {
|
|
currentDriver: DashboardDriverSummary | null;
|
|
myUpcomingRaces: DashboardRaceSummary[];
|
|
otherUpcomingRaces: DashboardRaceSummary[];
|
|
upcomingRaces: DashboardRaceSummary[];
|
|
activeLeaguesCount: number;
|
|
nextRace: DashboardRaceSummary | null;
|
|
recentResults: DashboardRecentRaceResultSummary[];
|
|
leagueStandingsSummaries: DashboardLeagueStandingSummary[];
|
|
feedSummary: DashboardFeedSummary;
|
|
friends: DashboardFriendSummary[];
|
|
}
|
|
|
|
export class DashboardOverviewUseCase {
|
|
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 getDriverAvatar: (driverId: string) => Promise<string>,
|
|
private readonly getDriverStats: (
|
|
driverId: string,
|
|
) => DashboardDriverStatsAdapter | null,
|
|
private readonly output: UseCaseOutputPort<DashboardOverviewResult>,
|
|
) {}
|
|
|
|
async execute(
|
|
input: DashboardOverviewInput,
|
|
): Promise<
|
|
Result<
|
|
void,
|
|
ApplicationErrorCode<'DRIVER_NOT_FOUND' | 'REPOSITORY_ERROR', { message: string }>
|
|
>
|
|
> {
|
|
const { driverId } = input;
|
|
|
|
try {
|
|
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),
|
|
]);
|
|
|
|
if (!driver) {
|
|
return Result.err({
|
|
code: 'DRIVER_NOT_FOUND',
|
|
details: { message: 'Driver not found' },
|
|
});
|
|
}
|
|
|
|
const leagueMap = new Map(allLeagues.map(league => [league.id.toString(), league]));
|
|
|
|
const driverStats = this.getDriverStats(driverId);
|
|
|
|
const currentDriver: DashboardDriverSummary = {
|
|
driver,
|
|
avatarUrl: await this.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,
|
|
};
|
|
|
|
const driverLeagues = await this.getDriverLeagues(allLeagues, driverId);
|
|
const driverLeagueIds = new Set(driverLeagues.map(league => league.id.toString()));
|
|
|
|
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.toString()),
|
|
);
|
|
|
|
const { myUpcomingRaces, otherUpcomingRaces } =
|
|
await this.partitionUpcomingRacesByRegistration(
|
|
upcomingRacesInDriverLeagues,
|
|
driverId,
|
|
leagueMap,
|
|
);
|
|
|
|
const nextRace: DashboardRaceSummary | null =
|
|
myUpcomingRaces.length > 0 ? myUpcomingRaces[0]! : null;
|
|
|
|
const upcomingRacesSummaries: DashboardRaceSummary[] = [
|
|
...myUpcomingRaces,
|
|
...otherUpcomingRaces,
|
|
]
|
|
.slice()
|
|
.sort(
|
|
(a, b) =>
|
|
a.race.scheduledAt.getTime() - b.race.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 = await this.buildFriendsSummary(friends);
|
|
|
|
const result: DashboardOverviewResult = {
|
|
currentDriver,
|
|
myUpcomingRaces,
|
|
otherUpcomingRaces,
|
|
upcomingRaces: upcomingRacesSummaries,
|
|
activeLeaguesCount,
|
|
nextRace,
|
|
recentResults,
|
|
leagueStandingsSummaries,
|
|
feedSummary,
|
|
friends: friendsSummary,
|
|
};
|
|
|
|
this.output.present(result);
|
|
|
|
return Result.ok(undefined);
|
|
} catch (error) {
|
|
return Result.err({
|
|
code: 'REPOSITORY_ERROR',
|
|
details: {
|
|
message: error instanceof Error ? error.message : 'Unknown error',
|
|
},
|
|
});
|
|
}
|
|
}
|
|
|
|
private async getDriverLeagues(
|
|
allLeagues: League[],
|
|
driverId: string,
|
|
): Promise<League[]> {
|
|
const driverLeagues: League[] = [];
|
|
|
|
for (const league of allLeagues) {
|
|
const membership = await this.leagueMembershipRepository.getMembership(
|
|
league.id.toString(),
|
|
driverId,
|
|
);
|
|
if (membership && membership.status.toString() === 'active') {
|
|
driverLeagues.push(league);
|
|
}
|
|
}
|
|
|
|
return driverLeagues;
|
|
}
|
|
|
|
private async partitionUpcomingRacesByRegistration(
|
|
upcomingRaces: Race[],
|
|
driverId: string,
|
|
leagueMap: Map<string, League>,
|
|
): Promise<{
|
|
myUpcomingRaces: DashboardRaceSummary[];
|
|
otherUpcomingRaces: DashboardRaceSummary[];
|
|
}> {
|
|
const myUpcomingRaces: DashboardRaceSummary[] = [];
|
|
const otherUpcomingRaces: DashboardRaceSummary[] = [];
|
|
|
|
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: Race,
|
|
leagueMap: Map<string, League>,
|
|
isMyLeague: boolean,
|
|
): DashboardRaceSummary {
|
|
return {
|
|
race,
|
|
league: leagueMap.get(race.leagueId) ?? null,
|
|
isMyLeague,
|
|
};
|
|
}
|
|
|
|
private buildRecentResults(
|
|
allResults: RaceResult[],
|
|
allRaces: Race[],
|
|
allLeagues: League[],
|
|
driverId: string,
|
|
): DashboardRecentRaceResultSummary[] {
|
|
const raceById = new Map(allRaces.map(race => [race.id, race]));
|
|
const leagueById = new Map(allLeagues.map(league => [league.id.toString(), league]));
|
|
|
|
const driverResults = allResults.filter(result => result.driverId.toString() === driverId);
|
|
|
|
const enriched = driverResults
|
|
.map(result => {
|
|
const race = raceById.get(result.raceId.toString());
|
|
if (!race) return null;
|
|
|
|
const league = leagueById.get(race.leagueId) ?? null;
|
|
|
|
const item: DashboardRecentRaceResultSummary = {
|
|
race,
|
|
league,
|
|
result,
|
|
};
|
|
|
|
return item;
|
|
})
|
|
.filter((item): item is DashboardRecentRaceResultSummary => !!item)
|
|
.sort(
|
|
(a, b) =>
|
|
b.race.scheduledAt.getTime() - a.race.scheduledAt.getTime(),
|
|
);
|
|
|
|
const RECENT_RESULTS_LIMIT = 5;
|
|
|
|
return enriched.slice(0, RECENT_RESULTS_LIMIT);
|
|
}
|
|
|
|
private async buildLeagueStandingsSummaries(
|
|
driverLeagues: League[],
|
|
driverId: string,
|
|
): Promise<DashboardLeagueStandingSummary[]> {
|
|
const summaries: DashboardLeagueStandingSummary[] = [];
|
|
|
|
for (const league of driverLeagues.slice(0, 3)) {
|
|
const standings = await this.standingRepository.findByLeagueId(league.id.toString());
|
|
const driverStanding = standings.find(
|
|
(standing: Standing) => standing.driverId.toString() === driverId,
|
|
);
|
|
|
|
summaries.push({
|
|
league,
|
|
standing: driverStanding ?? null,
|
|
totalDrivers: standings.length,
|
|
});
|
|
}
|
|
|
|
return summaries;
|
|
}
|
|
|
|
private computeActiveLeaguesCount(
|
|
upcomingRaces: DashboardRaceSummary[],
|
|
leagueStandingsSummaries: DashboardLeagueStandingSummary[],
|
|
): number {
|
|
const activeLeagueIds = new Set<string>();
|
|
|
|
for (const race of upcomingRaces) {
|
|
activeLeagueIds.add(race.race.leagueId);
|
|
}
|
|
|
|
for (const standing of leagueStandingsSummaries) {
|
|
activeLeagueIds.add(standing.league.id.toString());
|
|
}
|
|
|
|
return activeLeagueIds.size;
|
|
}
|
|
|
|
private buildFeedSummary(feedItems: FeedItem[]): DashboardFeedSummary {
|
|
return {
|
|
notificationCount: feedItems.length,
|
|
items: feedItems,
|
|
};
|
|
}
|
|
|
|
private async buildFriendsSummary(
|
|
friends: Driver[],
|
|
): Promise<DashboardFriendSummary[]> {
|
|
const friendSummaries: DashboardFriendSummary[] = [];
|
|
|
|
for (const friend of friends) {
|
|
const avatarUrl = await this.getDriverAvatar(friend.id);
|
|
friendSummaries.push({
|
|
driver: friend,
|
|
avatarUrl,
|
|
});
|
|
}
|
|
|
|
return friendSummaries;
|
|
}
|
|
}
|