view models
This commit is contained in:
@@ -1,111 +0,0 @@
|
||||
/**
|
||||
* AllLeaguesWithCapacityAndScoringPresenter - Pure data transformer
|
||||
* Transforms API response to view model without DI dependencies.
|
||||
*/
|
||||
|
||||
import { apiClient, type AllLeaguesWithCapacityViewModel } from '@/lib/apiClient';
|
||||
|
||||
export interface LeagueScoringViewModel {
|
||||
gameId: string;
|
||||
gameName: string;
|
||||
primaryChampionshipType: string;
|
||||
scoringPresetId: string;
|
||||
scoringPresetName: string;
|
||||
dropPolicySummary: string;
|
||||
scoringPatternSummary: string;
|
||||
}
|
||||
|
||||
export interface LeagueSummaryViewModel {
|
||||
id: string;
|
||||
name: string;
|
||||
description?: string | undefined;
|
||||
ownerId: string;
|
||||
createdAt: string;
|
||||
maxDrivers: number;
|
||||
usedDriverSlots: number;
|
||||
maxTeams: number;
|
||||
usedTeamSlots: number;
|
||||
structureSummary: string;
|
||||
scoringPatternSummary: string;
|
||||
timingSummary: string;
|
||||
scoring: LeagueScoringViewModel;
|
||||
}
|
||||
|
||||
export interface AllLeaguesWithCapacityAndScoringViewModel {
|
||||
leagues: LeagueSummaryViewModel[];
|
||||
totalCount: number;
|
||||
}
|
||||
|
||||
export interface IAllLeaguesWithCapacityAndScoringPresenter {
|
||||
reset(): void;
|
||||
getViewModel(): AllLeaguesWithCapacityAndScoringViewModel | null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Transform API response to view model
|
||||
*/
|
||||
function transformApiResponse(apiResponse: AllLeaguesWithCapacityViewModel): AllLeaguesWithCapacityAndScoringViewModel {
|
||||
const leagueItems: LeagueSummaryViewModel[] = apiResponse.leagues.map((league) => {
|
||||
const maxDrivers = league.maxMembers;
|
||||
const usedDriverSlots = league.memberCount;
|
||||
const structureSummary = `Solo • ${maxDrivers} drivers`;
|
||||
const timingSummary = '30 min Quali • 40 min Race';
|
||||
const scoringPatternSummary = 'Custom • All results count';
|
||||
|
||||
const scoringSummary: LeagueScoringViewModel = {
|
||||
gameId: 'unknown',
|
||||
gameName: 'Unknown',
|
||||
primaryChampionshipType: 'driver',
|
||||
scoringPresetId: 'custom',
|
||||
scoringPresetName: 'Custom',
|
||||
dropPolicySummary: 'All results count',
|
||||
scoringPatternSummary,
|
||||
};
|
||||
|
||||
return {
|
||||
id: league.id,
|
||||
name: league.name,
|
||||
description: league.description,
|
||||
ownerId: league.ownerId,
|
||||
createdAt: new Date().toISOString(), // Would need from API
|
||||
maxDrivers,
|
||||
usedDriverSlots,
|
||||
maxTeams: 0,
|
||||
usedTeamSlots: 0,
|
||||
structureSummary,
|
||||
scoringPatternSummary,
|
||||
timingSummary,
|
||||
scoring: scoringSummary,
|
||||
};
|
||||
});
|
||||
|
||||
return {
|
||||
leagues: leagueItems,
|
||||
totalCount: leagueItems.length,
|
||||
};
|
||||
}
|
||||
|
||||
export class AllLeaguesWithCapacityAndScoringPresenter implements IAllLeaguesWithCapacityAndScoringPresenter {
|
||||
private viewModel: AllLeaguesWithCapacityAndScoringViewModel | null = null;
|
||||
|
||||
reset(): void {
|
||||
this.viewModel = null;
|
||||
}
|
||||
|
||||
async fetchAndPresent(): Promise<void> {
|
||||
const apiResponse = await apiClient.leagues.getAllWithCapacity();
|
||||
this.viewModel = transformApiResponse(apiResponse);
|
||||
}
|
||||
|
||||
getViewModel(): AllLeaguesWithCapacityAndScoringViewModel | null {
|
||||
return this.viewModel;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Convenience function to fetch and transform all leagues
|
||||
*/
|
||||
export async function fetchAllLeaguesWithCapacityAndScoring(): Promise<AllLeaguesWithCapacityAndScoringViewModel> {
|
||||
const apiResponse = await apiClient.leagues.getAllWithCapacity();
|
||||
return transformApiResponse(apiResponse);
|
||||
}
|
||||
@@ -1,73 +0,0 @@
|
||||
import type {
|
||||
IAllLeaguesWithCapacityPresenter,
|
||||
LeagueWithCapacityViewModel,
|
||||
AllLeaguesWithCapacityViewModel,
|
||||
AllLeaguesWithCapacityResultDTO,
|
||||
} from '@core/racing/application/presenters/IAllLeaguesWithCapacityPresenter';
|
||||
|
||||
export class AllLeaguesWithCapacityPresenter implements IAllLeaguesWithCapacityPresenter {
|
||||
private viewModel: AllLeaguesWithCapacityViewModel | null = null;
|
||||
|
||||
reset(): void {
|
||||
this.viewModel = null;
|
||||
}
|
||||
|
||||
present(input: AllLeaguesWithCapacityResultDTO): void {
|
||||
const { leagues, memberCounts } = input;
|
||||
const leagueItems: LeagueWithCapacityViewModel[] = leagues.map((league) => {
|
||||
const usedSlots = memberCounts.get(league.id) ?? 0;
|
||||
|
||||
// Ensure we never expose an impossible state like 26/24:
|
||||
// clamp maxDrivers to at least usedSlots at the application boundary.
|
||||
const configuredMax = league.settings.maxDrivers ?? usedSlots;
|
||||
const safeMaxDrivers = Math.max(configuredMax, usedSlots);
|
||||
|
||||
const base: LeagueWithCapacityViewModel = {
|
||||
id: league.id,
|
||||
name: league.name,
|
||||
description: league.description,
|
||||
ownerId: league.ownerId,
|
||||
settings: {
|
||||
...league.settings,
|
||||
maxDrivers: safeMaxDrivers,
|
||||
},
|
||||
createdAt: league.createdAt.toISOString(),
|
||||
usedSlots,
|
||||
};
|
||||
|
||||
if (!league.socialLinks) {
|
||||
return base;
|
||||
}
|
||||
|
||||
const socialLinks: NonNullable<LeagueWithCapacityViewModel['socialLinks']> = {};
|
||||
|
||||
if (league.socialLinks.discordUrl) {
|
||||
socialLinks.discordUrl = league.socialLinks.discordUrl;
|
||||
}
|
||||
if (league.socialLinks.youtubeUrl) {
|
||||
socialLinks.youtubeUrl = league.socialLinks.youtubeUrl;
|
||||
}
|
||||
if (league.socialLinks.websiteUrl) {
|
||||
socialLinks.websiteUrl = league.socialLinks.websiteUrl;
|
||||
}
|
||||
|
||||
if (Object.keys(socialLinks).length === 0) {
|
||||
return base;
|
||||
}
|
||||
|
||||
return {
|
||||
...base,
|
||||
socialLinks,
|
||||
};
|
||||
});
|
||||
|
||||
this.viewModel = {
|
||||
leagues: leagueItems,
|
||||
totalCount: leagueItems.length,
|
||||
};
|
||||
}
|
||||
|
||||
getViewModel(): AllLeaguesWithCapacityViewModel | null {
|
||||
return this.viewModel;
|
||||
}
|
||||
}
|
||||
@@ -1,21 +0,0 @@
|
||||
import type {
|
||||
IAllRacesPagePresenter,
|
||||
AllRacesPageResultDTO,
|
||||
AllRacesPageViewModel,
|
||||
} from '@core/racing/application/presenters/IAllRacesPagePresenter';
|
||||
|
||||
export class AllRacesPagePresenter implements IAllRacesPagePresenter {
|
||||
private viewModel: AllRacesPageViewModel | null = null;
|
||||
|
||||
reset(): void {
|
||||
this.viewModel = null;
|
||||
}
|
||||
|
||||
present(dto: AllRacesPageResultDTO): void {
|
||||
this.viewModel = dto;
|
||||
}
|
||||
|
||||
getViewModel(): AllRacesPageViewModel | null {
|
||||
return this.viewModel;
|
||||
}
|
||||
}
|
||||
@@ -1,78 +0,0 @@
|
||||
/**
|
||||
* AllTeamsPresenter - Pure data transformer
|
||||
* Transforms API response to view model without DI dependencies.
|
||||
*/
|
||||
|
||||
import { apiClient, type AllTeamsViewModel as ApiAllTeamsViewModel } from '@/lib/apiClient';
|
||||
|
||||
export interface TeamListItemViewModel {
|
||||
id: string;
|
||||
name: string;
|
||||
tag?: string | undefined;
|
||||
description?: string | undefined;
|
||||
memberCount: number;
|
||||
logoUrl?: string | undefined;
|
||||
rating?: number | undefined;
|
||||
}
|
||||
|
||||
export interface AllTeamsViewModel {
|
||||
teams: TeamListItemViewModel[];
|
||||
totalCount: number;
|
||||
}
|
||||
|
||||
export interface IAllTeamsPresenter {
|
||||
reset(): void;
|
||||
getViewModel(): AllTeamsViewModel | null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Transform API response to view model
|
||||
*/
|
||||
function transformApiResponse(apiResponse: ApiAllTeamsViewModel): AllTeamsViewModel {
|
||||
const teamItems: TeamListItemViewModel[] = apiResponse.teams.map((team) => {
|
||||
const viewModel: TeamListItemViewModel = {
|
||||
id: team.id,
|
||||
name: team.name,
|
||||
memberCount: team.memberCount ?? 0,
|
||||
};
|
||||
|
||||
if (team.logoUrl) {
|
||||
viewModel.logoUrl = team.logoUrl;
|
||||
}
|
||||
if (team.rating) {
|
||||
viewModel.rating = team.rating;
|
||||
}
|
||||
|
||||
return viewModel;
|
||||
});
|
||||
|
||||
return {
|
||||
teams: teamItems,
|
||||
totalCount: teamItems.length,
|
||||
};
|
||||
}
|
||||
|
||||
export class AllTeamsPresenter implements IAllTeamsPresenter {
|
||||
private viewModel: AllTeamsViewModel | null = null;
|
||||
|
||||
reset(): void {
|
||||
this.viewModel = null;
|
||||
}
|
||||
|
||||
async fetchAndPresent(): Promise<void> {
|
||||
const apiResponse = await apiClient.teams.getAll();
|
||||
this.viewModel = transformApiResponse(apiResponse);
|
||||
}
|
||||
|
||||
getViewModel(): AllTeamsViewModel | null {
|
||||
return this.viewModel;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Convenience function to fetch and transform all teams
|
||||
*/
|
||||
export async function fetchAllTeams(): Promise<AllTeamsViewModel> {
|
||||
const apiResponse = await apiClient.teams.getAll();
|
||||
return transformApiResponse(apiResponse);
|
||||
}
|
||||
@@ -1,8 +0,0 @@
|
||||
import { AnalyticsDashboardDto } from '../dtos';
|
||||
import { AnalyticsDashboardViewModel } from '../view-models';
|
||||
|
||||
export class AnalyticsDashboardPresenter {
|
||||
present(dto: AnalyticsDashboardDto): AnalyticsDashboardViewModel {
|
||||
return new AnalyticsDashboardViewModel(dto);
|
||||
}
|
||||
}
|
||||
@@ -1,8 +0,0 @@
|
||||
import { AnalyticsMetricsDto } from '../dtos';
|
||||
import { AnalyticsMetricsViewModel } from '../view-models';
|
||||
|
||||
export class AnalyticsMetricsPresenter {
|
||||
present(dto: AnalyticsMetricsDto): AnalyticsMetricsViewModel {
|
||||
return new AnalyticsMetricsViewModel(dto);
|
||||
}
|
||||
}
|
||||
@@ -1,39 +0,0 @@
|
||||
import type {
|
||||
GetAvatarOutputDto,
|
||||
RequestAvatarGenerationOutputDto,
|
||||
UpdateAvatarOutputDto
|
||||
} from '../dtos';
|
||||
import type {
|
||||
AvatarViewModel,
|
||||
RequestAvatarGenerationViewModel,
|
||||
UpdateAvatarViewModel
|
||||
} from '../view-models';
|
||||
|
||||
/**
|
||||
* Avatar Presenter
|
||||
* Transforms avatar DTOs to ViewModels
|
||||
*/
|
||||
export class AvatarPresenter {
|
||||
presentAvatar(dto: GetAvatarOutputDto): AvatarViewModel {
|
||||
return {
|
||||
driverId: dto.driverId,
|
||||
avatarUrl: dto.avatarUrl,
|
||||
hasAvatar: dto.hasAvatar,
|
||||
};
|
||||
}
|
||||
|
||||
presentRequestGeneration(dto: RequestAvatarGenerationOutputDto): RequestAvatarGenerationViewModel {
|
||||
return {
|
||||
success: dto.success,
|
||||
avatarUrl: dto.avatarUrl,
|
||||
error: dto.error,
|
||||
};
|
||||
}
|
||||
|
||||
presentUpdate(dto: UpdateAvatarOutputDto): UpdateAvatarViewModel {
|
||||
return {
|
||||
success: dto.success,
|
||||
error: dto.error,
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -1,15 +0,0 @@
|
||||
import type { CompleteOnboardingOutputDto } from '../dtos';
|
||||
import type { CompleteOnboardingViewModel } from '../view-models/CompleteOnboardingViewModel';
|
||||
|
||||
/**
|
||||
* Complete Onboarding Presenter
|
||||
* Transforms CompleteOnboardingOutputDto to CompleteOnboardingViewModel
|
||||
*/
|
||||
export class CompleteOnboardingPresenter {
|
||||
present(dto: CompleteOnboardingOutputDto): CompleteOnboardingViewModel {
|
||||
return {
|
||||
driverId: dto.driverId,
|
||||
success: dto.success,
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -1,21 +0,0 @@
|
||||
import type {
|
||||
IDashboardOverviewPresenter,
|
||||
DashboardOverviewResultDTO,
|
||||
DashboardOverviewViewModel,
|
||||
} from '@core/racing/application/presenters/IDashboardOverviewPresenter';
|
||||
|
||||
export class DashboardOverviewPresenter implements IDashboardOverviewPresenter {
|
||||
private viewModel: DashboardOverviewViewModel | null = null;
|
||||
|
||||
reset(): void {
|
||||
this.viewModel = null;
|
||||
}
|
||||
|
||||
present(dto: DashboardOverviewResultDTO): void {
|
||||
this.viewModel = dto;
|
||||
}
|
||||
|
||||
getViewModel(): DashboardOverviewViewModel | null {
|
||||
return this.viewModel;
|
||||
}
|
||||
}
|
||||
@@ -1,18 +0,0 @@
|
||||
import type { DriverDto } from '../dtos';
|
||||
import type { DriverViewModel } from '../view-models/DriverViewModel';
|
||||
|
||||
/**
|
||||
* Driver Presenter
|
||||
* Transforms DriverDto to DriverViewModel
|
||||
*/
|
||||
export class DriverPresenter {
|
||||
present(dto: DriverDto): DriverViewModel {
|
||||
return {
|
||||
id: dto.id,
|
||||
name: dto.name,
|
||||
avatarUrl: dto.avatarUrl,
|
||||
iracingId: dto.iracingId,
|
||||
rating: dto.rating,
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -1,19 +0,0 @@
|
||||
import type { DriverRegistrationStatusDto } from '../dtos';
|
||||
import type { DriverRegistrationStatusViewModel } from '../view-models';
|
||||
import { DriverRegistrationStatusViewModel as ViewModel } from '../view-models';
|
||||
|
||||
/**
|
||||
* Driver Registration Status Presenter
|
||||
* Transforms DriverRegistrationStatusDto to DriverRegistrationStatusViewModel
|
||||
*/
|
||||
export class DriverRegistrationStatusPresenter {
|
||||
present(dto: DriverRegistrationStatusDto): DriverRegistrationStatusViewModel {
|
||||
return new ViewModel(dto);
|
||||
}
|
||||
}
|
||||
|
||||
// Legacy functional export for backward compatibility
|
||||
export const presentDriverRegistrationStatus = (dto: DriverRegistrationStatusDto): DriverRegistrationStatusViewModel => {
|
||||
const presenter = new DriverRegistrationStatusPresenter();
|
||||
return presenter.present(dto);
|
||||
};
|
||||
@@ -1,88 +0,0 @@
|
||||
/**
|
||||
* DriverTeamPresenter - Pure data transformer
|
||||
* Transforms API response to view model without DI dependencies.
|
||||
*/
|
||||
|
||||
import { apiClient, type DriverTeamViewModel as ApiDriverTeamViewModel } from '@/lib/apiClient';
|
||||
|
||||
export interface DriverTeamMembershipViewModel {
|
||||
role: string;
|
||||
joinedAt: string;
|
||||
isActive: boolean;
|
||||
}
|
||||
|
||||
export interface DriverTeamInfoViewModel {
|
||||
id: string;
|
||||
name: string;
|
||||
tag?: string | undefined;
|
||||
description?: string | undefined;
|
||||
ownerId: string;
|
||||
leagues?: string[] | undefined;
|
||||
}
|
||||
|
||||
export interface DriverTeamViewModel {
|
||||
team: DriverTeamInfoViewModel;
|
||||
membership: DriverTeamMembershipViewModel;
|
||||
isOwner: boolean;
|
||||
canManage: boolean;
|
||||
}
|
||||
|
||||
export interface IDriverTeamPresenter {
|
||||
reset(): void;
|
||||
getViewModel(): DriverTeamViewModel | null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Transform API response to view model
|
||||
*/
|
||||
function transformApiResponse(apiResponse: ApiDriverTeamViewModel): DriverTeamViewModel {
|
||||
const isOwner = false; // Would need team owner info from API
|
||||
const canManage = apiResponse.role === 'owner' || apiResponse.role === 'manager';
|
||||
|
||||
return {
|
||||
team: {
|
||||
id: apiResponse.teamId,
|
||||
name: apiResponse.teamName,
|
||||
ownerId: '', // Would need from API
|
||||
},
|
||||
membership: {
|
||||
role: apiResponse.role === 'driver' ? 'member' : apiResponse.role,
|
||||
joinedAt: new Date(apiResponse.joinedAt).toISOString(),
|
||||
isActive: true,
|
||||
},
|
||||
isOwner,
|
||||
canManage,
|
||||
};
|
||||
}
|
||||
|
||||
export class DriverTeamPresenter implements IDriverTeamPresenter {
|
||||
private viewModel: DriverTeamViewModel | null = null;
|
||||
|
||||
reset(): void {
|
||||
this.viewModel = null;
|
||||
}
|
||||
|
||||
async fetchAndPresent(driverId: string): Promise<void> {
|
||||
const apiResponse = await apiClient.teams.getDriverTeam(driverId);
|
||||
if (apiResponse) {
|
||||
this.viewModel = transformApiResponse(apiResponse);
|
||||
} else {
|
||||
this.viewModel = null;
|
||||
}
|
||||
}
|
||||
|
||||
getViewModel(): DriverTeamViewModel | null {
|
||||
return this.viewModel;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Convenience function to fetch and transform driver's team
|
||||
*/
|
||||
export async function fetchDriverTeam(driverId: string): Promise<DriverTeamViewModel | null> {
|
||||
const apiResponse = await apiClient.teams.getDriverTeam(driverId);
|
||||
if (!apiResponse) {
|
||||
return null;
|
||||
}
|
||||
return transformApiResponse(apiResponse);
|
||||
}
|
||||
@@ -1,18 +0,0 @@
|
||||
import type { DriversLeaderboardDto } from '../dtos';
|
||||
import type { DriverLeaderboardViewModel } from '../view-models';
|
||||
|
||||
/**
|
||||
* Drivers Leaderboard Presenter
|
||||
* Transforms DriversLeaderboardDto to DriverLeaderboardViewModel
|
||||
*/
|
||||
export class DriversLeaderboardPresenter {
|
||||
present(dto: DriversLeaderboardDto, previousDrivers?: any): DriverLeaderboardViewModel {
|
||||
return new DriverLeaderboardViewModel(dto as any, previousDrivers);
|
||||
}
|
||||
}
|
||||
|
||||
// Legacy functional export for backward compatibility
|
||||
export const presentDriversLeaderboard = (dto: DriversLeaderboardDto, previousDrivers?: any): DriverLeaderboardViewModel => {
|
||||
const presenter = new DriversLeaderboardPresenter();
|
||||
return presenter.present(dto, previousDrivers);
|
||||
};
|
||||
@@ -1,14 +0,0 @@
|
||||
import type { IEntitySponsorshipPricingPresenter } from '@core/racing/application/presenters/IEntitySponsorshipPricingPresenter';
|
||||
import type { GetEntitySponsorshipPricingResultDTO } from '@core/racing/application/use-cases/GetEntitySponsorshipPricingUseCase';
|
||||
|
||||
export class EntitySponsorshipPricingPresenter implements IEntitySponsorshipPricingPresenter {
|
||||
private data: GetEntitySponsorshipPricingResultDTO | null = null;
|
||||
|
||||
present(data: GetEntitySponsorshipPricingResultDTO | null): void {
|
||||
this.data = data;
|
||||
}
|
||||
|
||||
getData(): GetEntitySponsorshipPricingResultDTO | null {
|
||||
return this.data;
|
||||
}
|
||||
}
|
||||
@@ -1,21 +0,0 @@
|
||||
import type { ImportRaceResultsSummaryDto } from '../dtos/ImportRaceResultsSummaryDto';
|
||||
|
||||
export interface ImportRaceResultsSummaryViewModel {
|
||||
success: boolean;
|
||||
raceId: string;
|
||||
driversProcessed: number;
|
||||
resultsRecorded: number;
|
||||
errors?: string[];
|
||||
}
|
||||
|
||||
export class ImportRaceResultsPresenter {
|
||||
present(dto: ImportRaceResultsSummaryDto): ImportRaceResultsSummaryViewModel {
|
||||
return {
|
||||
success: dto.success,
|
||||
raceId: dto.raceId,
|
||||
driversProcessed: dto.driversProcessed,
|
||||
resultsRecorded: dto.resultsRecorded,
|
||||
errors: dto.errors,
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -1,269 +0,0 @@
|
||||
/**
|
||||
* LeagueAdminPresenter - Pure data transformer
|
||||
* Transforms API responses to view models without DI dependencies.
|
||||
* All data fetching is done via apiClient.
|
||||
*/
|
||||
|
||||
import { apiClient } from '@/lib/apiClient';
|
||||
import type {
|
||||
LeagueJoinRequestViewModel as ApiLeagueJoinRequestViewModel,
|
||||
LeagueConfigFormModelDto,
|
||||
LeagueSeasonSummaryViewModel as ApiLeagueSeasonSummaryViewModel,
|
||||
DriverDTO,
|
||||
} from '@/lib/apiClient';
|
||||
|
||||
// ============================================================================
|
||||
// View Model Types
|
||||
// ============================================================================
|
||||
|
||||
export interface LeagueJoinRequestViewModel {
|
||||
id: string;
|
||||
leagueId: string;
|
||||
driverId: string;
|
||||
requestedAt: Date;
|
||||
message?: string | undefined;
|
||||
driver?: DriverDTO | undefined;
|
||||
}
|
||||
|
||||
export interface ProtestDriverSummary {
|
||||
[driverId: string]: DriverDTO;
|
||||
}
|
||||
|
||||
export interface ProtestRaceSummary {
|
||||
[raceId: string]: {
|
||||
id: string;
|
||||
name: string;
|
||||
scheduledTime: string;
|
||||
};
|
||||
}
|
||||
|
||||
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: Array<{
|
||||
id: string;
|
||||
raceId: string;
|
||||
complainantId: string;
|
||||
defendantId: string;
|
||||
description: string;
|
||||
status: string;
|
||||
createdAt: string;
|
||||
}>;
|
||||
racesById: ProtestRaceSummary;
|
||||
driversById: ProtestDriverSummary;
|
||||
}
|
||||
|
||||
export interface LeagueAdminConfigViewModel {
|
||||
form: LeagueConfigFormModelDto | null;
|
||||
}
|
||||
|
||||
export interface LeagueAdminPermissionsViewModel {
|
||||
canRemoveMember: boolean;
|
||||
canUpdateRoles: boolean;
|
||||
}
|
||||
|
||||
export interface LeagueSeasonSummaryViewModel {
|
||||
seasonId: string;
|
||||
name: string;
|
||||
status: string;
|
||||
startDate?: Date | undefined;
|
||||
endDate?: Date | undefined;
|
||||
isPrimary: boolean;
|
||||
isParallelActive: boolean;
|
||||
}
|
||||
|
||||
export interface LeagueAdminViewModel {
|
||||
joinRequests: LeagueJoinRequestViewModel[];
|
||||
ownerSummary: LeagueOwnerSummaryViewModel | null;
|
||||
config: LeagueAdminConfigViewModel;
|
||||
protests: LeagueAdminProtestsViewModel;
|
||||
}
|
||||
|
||||
export type MembershipRole = 'owner' | 'admin' | 'member';
|
||||
|
||||
// ============================================================================
|
||||
// Data Fetching Functions (using apiClient)
|
||||
// ============================================================================
|
||||
|
||||
/**
|
||||
* Load join requests for a league via API.
|
||||
*/
|
||||
export async function loadLeagueJoinRequests(leagueId: string): Promise<LeagueJoinRequestViewModel[]> {
|
||||
const requests = await apiClient.leagues.getJoinRequests(leagueId);
|
||||
|
||||
return requests.map((request: ApiLeagueJoinRequestViewModel) => {
|
||||
const viewModel: LeagueJoinRequestViewModel = {
|
||||
id: request.id,
|
||||
leagueId: request.leagueId,
|
||||
driverId: request.driverId,
|
||||
requestedAt: new Date(request.requestedAt),
|
||||
};
|
||||
|
||||
if (request.message) {
|
||||
viewModel.message = request.message;
|
||||
}
|
||||
|
||||
return viewModel;
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Approve a league join request and return updated join requests.
|
||||
*/
|
||||
export async function approveLeagueJoinRequest(
|
||||
leagueId: string,
|
||||
requestId: string
|
||||
): Promise<LeagueJoinRequestViewModel[]> {
|
||||
await apiClient.leagues.approveJoinRequest(leagueId, requestId);
|
||||
return loadLeagueJoinRequests(leagueId);
|
||||
}
|
||||
|
||||
/**
|
||||
* Reject a league join request.
|
||||
*/
|
||||
export async function rejectLeagueJoinRequest(
|
||||
leagueId: string,
|
||||
requestId: string
|
||||
): Promise<LeagueJoinRequestViewModel[]> {
|
||||
await apiClient.leagues.rejectJoinRequest(leagueId, requestId);
|
||||
return loadLeagueJoinRequests(leagueId);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get permissions for a performer on league membership actions.
|
||||
*/
|
||||
export async function getLeagueAdminPermissions(
|
||||
leagueId: string,
|
||||
performerDriverId: string
|
||||
): Promise<LeagueAdminPermissionsViewModel> {
|
||||
const permissions = await apiClient.leagues.getAdminPermissions(leagueId, performerDriverId);
|
||||
|
||||
return {
|
||||
canRemoveMember: permissions.canManageMembers || permissions.isOwner || permissions.isAdmin,
|
||||
canUpdateRoles: permissions.isOwner,
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Remove a member from the league.
|
||||
*/
|
||||
export async function removeLeagueMember(
|
||||
leagueId: string,
|
||||
performerDriverId: string,
|
||||
targetDriverId: string
|
||||
): Promise<void> {
|
||||
await apiClient.leagues.removeMember(leagueId, performerDriverId, targetDriverId);
|
||||
}
|
||||
|
||||
/**
|
||||
* Update a member's role.
|
||||
*/
|
||||
export async function updateLeagueMemberRole(
|
||||
leagueId: string,
|
||||
performerDriverId: string,
|
||||
targetDriverId: string,
|
||||
newRole: MembershipRole
|
||||
): Promise<void> {
|
||||
await apiClient.leagues.updateMemberRole(leagueId, performerDriverId, targetDriverId, newRole);
|
||||
}
|
||||
|
||||
/**
|
||||
* Load owner summary for a league.
|
||||
*/
|
||||
export async function loadLeagueOwnerSummary(params: {
|
||||
leagueId: string;
|
||||
ownerId: string;
|
||||
}): Promise<LeagueOwnerSummaryViewModel | null> {
|
||||
const ownerSummary = await apiClient.leagues.getOwnerSummary(params.leagueId, params.ownerId);
|
||||
|
||||
if (!ownerSummary) {
|
||||
return null;
|
||||
}
|
||||
|
||||
// For now, return a simplified version - the API should provide driver details
|
||||
return {
|
||||
driver: {
|
||||
id: params.ownerId,
|
||||
name: ownerSummary.leagueName, // This would need to be populated from API
|
||||
},
|
||||
rating: null,
|
||||
rank: null,
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Load league full config form.
|
||||
*/
|
||||
export async function loadLeagueConfig(
|
||||
leagueId: string
|
||||
): Promise<LeagueAdminConfigViewModel> {
|
||||
const config = await apiClient.leagues.getConfig(leagueId);
|
||||
|
||||
return {
|
||||
form: config,
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Load protests for a league.
|
||||
*/
|
||||
export async function loadLeagueProtests(leagueId: string): Promise<LeagueAdminProtestsViewModel> {
|
||||
const protestsData = await apiClient.leagues.getProtests(leagueId);
|
||||
|
||||
// Transform the API response
|
||||
const racesById: ProtestRaceSummary = {};
|
||||
const driversById: ProtestDriverSummary = {};
|
||||
|
||||
return {
|
||||
protests: protestsData.protests.map((p) => ({
|
||||
id: p.id,
|
||||
raceId: p.raceId,
|
||||
complainantId: p.complainantId,
|
||||
defendantId: p.defendantId,
|
||||
description: p.description,
|
||||
status: p.status,
|
||||
createdAt: p.createdAt,
|
||||
})),
|
||||
racesById,
|
||||
driversById,
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Load seasons for a league.
|
||||
*/
|
||||
export async function loadLeagueSeasons(leagueId: string): Promise<LeagueSeasonSummaryViewModel[]> {
|
||||
const seasons = await apiClient.leagues.getSeasons(leagueId);
|
||||
const activeCount = seasons.filter((s: ApiLeagueSeasonSummaryViewModel) => s.status === 'active').length;
|
||||
|
||||
return seasons.map((s: ApiLeagueSeasonSummaryViewModel) => {
|
||||
const viewModel: LeagueSeasonSummaryViewModel = {
|
||||
seasonId: s.id,
|
||||
name: s.name,
|
||||
status: s.status,
|
||||
isPrimary: false, // Would need to be provided by API
|
||||
isParallelActive: activeCount > 1 && s.status === 'active',
|
||||
};
|
||||
|
||||
if (s.startDate) {
|
||||
viewModel.startDate = new Date(s.startDate);
|
||||
}
|
||||
if (s.endDate) {
|
||||
viewModel.endDate = new Date(s.endDate);
|
||||
}
|
||||
|
||||
return viewModel;
|
||||
});
|
||||
}
|
||||
@@ -1,72 +0,0 @@
|
||||
import type {
|
||||
ILeagueDriverSeasonStatsPresenter,
|
||||
LeagueDriverSeasonStatsItemViewModel,
|
||||
LeagueDriverSeasonStatsViewModel,
|
||||
LeagueDriverSeasonStatsResultDTO,
|
||||
} from '@core/racing/application/presenters/ILeagueDriverSeasonStatsPresenter';
|
||||
|
||||
export class LeagueDriverSeasonStatsPresenter implements ILeagueDriverSeasonStatsPresenter {
|
||||
private viewModel: LeagueDriverSeasonStatsViewModel | null = null;
|
||||
|
||||
reset(): void {
|
||||
this.viewModel = null;
|
||||
}
|
||||
|
||||
present(dto: LeagueDriverSeasonStatsResultDTO): void {
|
||||
const { leagueId, standings, penalties, driverResults, driverRatings } = dto;
|
||||
|
||||
const stats: LeagueDriverSeasonStatsItemViewModel[] = standings.map((standing) => {
|
||||
const penalty = penalties.get(standing.driverId) ?? { baseDelta: 0, bonusDelta: 0 };
|
||||
const totalPenaltyPoints = penalty.baseDelta;
|
||||
const bonusPoints = penalty.bonusDelta;
|
||||
|
||||
const racesCompleted = standing.racesCompleted;
|
||||
const pointsPerRace = racesCompleted > 0 ? standing.points / racesCompleted : 0;
|
||||
|
||||
const ratingInfo = driverRatings.get(standing.driverId) ?? { rating: null, ratingChange: null };
|
||||
|
||||
const results = driverResults.get(standing.driverId) ?? [];
|
||||
let avgFinish: number | null = null;
|
||||
if (results.length > 0) {
|
||||
const totalPositions = results.reduce((sum, r) => sum + r.position, 0);
|
||||
const avg = totalPositions / results.length;
|
||||
avgFinish = Number.isFinite(avg) ? Number(avg.toFixed(2)) : null;
|
||||
}
|
||||
|
||||
return {
|
||||
leagueId,
|
||||
driverId: standing.driverId,
|
||||
position: standing.position,
|
||||
driverName: '',
|
||||
teamId: '',
|
||||
teamName: '',
|
||||
totalPoints: standing.points + totalPenaltyPoints + bonusPoints,
|
||||
basePoints: standing.points,
|
||||
penaltyPoints: Math.abs(totalPenaltyPoints),
|
||||
bonusPoints,
|
||||
pointsPerRace,
|
||||
racesStarted: results.length,
|
||||
racesFinished: results.length,
|
||||
dnfs: 0,
|
||||
noShows: 0,
|
||||
avgFinish,
|
||||
rating: ratingInfo.rating,
|
||||
ratingChange: ratingInfo.ratingChange,
|
||||
};
|
||||
});
|
||||
|
||||
stats.sort((a, b) => a.position - b.position);
|
||||
|
||||
this.viewModel = {
|
||||
leagueId,
|
||||
stats,
|
||||
};
|
||||
}
|
||||
|
||||
getViewModel(): LeagueDriverSeasonStatsViewModel {
|
||||
if (!this.viewModel) {
|
||||
throw new Error('Presenter has not been called yet');
|
||||
}
|
||||
return this.viewModel;
|
||||
}
|
||||
}
|
||||
@@ -1,117 +0,0 @@
|
||||
import type { DropScorePolicy } from '@core/racing/domain/types/DropScorePolicy';
|
||||
import type {
|
||||
ILeagueFullConfigPresenter,
|
||||
LeagueFullConfigData,
|
||||
LeagueConfigFormViewModel,
|
||||
} from '@core/racing/application/presenters/ILeagueFullConfigPresenter';
|
||||
|
||||
export class LeagueFullConfigPresenter implements ILeagueFullConfigPresenter {
|
||||
private viewModel: LeagueConfigFormViewModel | null = null;
|
||||
|
||||
reset(): void {
|
||||
this.viewModel = null;
|
||||
}
|
||||
|
||||
present(data: LeagueFullConfigData): void {
|
||||
const { league, activeSeason, scoringConfig, game } = data;
|
||||
|
||||
const patternId = scoringConfig?.scoringPresetId;
|
||||
|
||||
const primaryChampionship =
|
||||
scoringConfig && scoringConfig.championships && scoringConfig.championships.length > 0
|
||||
? scoringConfig.championships[0]
|
||||
: undefined;
|
||||
|
||||
const dropPolicy = primaryChampionship?.dropScorePolicy ?? undefined;
|
||||
const dropPolicyForm = this.mapDropPolicy(dropPolicy);
|
||||
|
||||
const defaultQualifyingMinutes = 30;
|
||||
const defaultMainRaceMinutes = 40;
|
||||
const mainRaceMinutes =
|
||||
typeof league.settings.sessionDuration === 'number'
|
||||
? league.settings.sessionDuration
|
||||
: defaultMainRaceMinutes;
|
||||
const qualifyingMinutes = defaultQualifyingMinutes;
|
||||
|
||||
const roundsPlanned = 8;
|
||||
|
||||
let sessionCount = 2;
|
||||
if (primaryChampionship && Array.isArray(primaryChampionship.sessionTypes)) {
|
||||
sessionCount = primaryChampionship.sessionTypes.length;
|
||||
}
|
||||
|
||||
const practiceMinutes = 20;
|
||||
const sprintRaceMinutes = patternId === 'sprint-main-driver' ? 20 : undefined;
|
||||
|
||||
this.viewModel = {
|
||||
leagueId: league.id,
|
||||
basics: {
|
||||
name: league.name,
|
||||
description: league.description,
|
||||
visibility: 'public',
|
||||
gameId: game?.id ?? 'iracing',
|
||||
},
|
||||
structure: {
|
||||
mode: 'solo',
|
||||
maxDrivers: league.settings.maxDrivers ?? 32,
|
||||
multiClassEnabled: false,
|
||||
},
|
||||
championships: {
|
||||
enableDriverChampionship: true,
|
||||
enableTeamChampionship: false,
|
||||
enableNationsChampionship: false,
|
||||
enableTrophyChampionship: false,
|
||||
},
|
||||
scoring: {
|
||||
customScoringEnabled: !patternId,
|
||||
...(patternId ? { patternId } : {}),
|
||||
},
|
||||
dropPolicy: dropPolicyForm,
|
||||
timings: {
|
||||
practiceMinutes,
|
||||
qualifyingMinutes,
|
||||
mainRaceMinutes,
|
||||
sessionCount,
|
||||
roundsPlanned,
|
||||
...(typeof sprintRaceMinutes === 'number'
|
||||
? { sprintRaceMinutes }
|
||||
: {}),
|
||||
},
|
||||
stewarding: {
|
||||
decisionMode: 'admin_only',
|
||||
requireDefense: true,
|
||||
defenseTimeLimit: 48,
|
||||
voteTimeLimit: 72,
|
||||
protestDeadlineHours: 72,
|
||||
stewardingClosesHours: 168,
|
||||
notifyAccusedOnProtest: true,
|
||||
notifyOnVoteRequired: true,
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
getViewModel(): LeagueConfigFormViewModel | null {
|
||||
if (!this.viewModel) {
|
||||
throw new Error('Presenter has not been called yet');
|
||||
}
|
||||
return this.viewModel;
|
||||
}
|
||||
|
||||
private mapDropPolicy(policy: DropScorePolicy | undefined): { strategy: string; n?: number } {
|
||||
if (!policy || policy.strategy === 'none') {
|
||||
return { strategy: 'none' };
|
||||
}
|
||||
|
||||
if (policy.strategy === 'bestNResults') {
|
||||
const n = typeof policy.count === 'number' ? policy.count : undefined;
|
||||
return n !== undefined ? { strategy: 'bestNResults', n } : { strategy: 'none' };
|
||||
}
|
||||
|
||||
if (policy.strategy === 'dropWorstN') {
|
||||
const n = typeof policy.dropCount === 'number' ? policy.dropCount : undefined;
|
||||
return n !== undefined ? { strategy: 'dropWorstN', n } : { strategy: 'none' };
|
||||
}
|
||||
|
||||
return { strategy: 'none' };
|
||||
}
|
||||
}
|
||||
@@ -1,6 +0,0 @@
|
||||
import { LeagueMemberDto } from '../dtos';
|
||||
import { LeagueMemberViewModel } from '../view-models';
|
||||
|
||||
export const presentLeagueMember = (dto: LeagueMemberDto, currentUserId: string): LeagueMemberViewModel => {
|
||||
return new LeagueMemberViewModel(dto, currentUserId);
|
||||
};
|
||||
@@ -1,16 +0,0 @@
|
||||
import type { LeagueMembershipsDto } from '../dtos';
|
||||
import { LeagueMemberViewModel } from '../view-models';
|
||||
|
||||
/**
|
||||
* League Members Presenter
|
||||
*
|
||||
* Transforms league memberships DTO to view models for the UI.
|
||||
*/
|
||||
export class LeagueMembersPresenter {
|
||||
/**
|
||||
* Present league memberships with current user context
|
||||
*/
|
||||
present(dto: LeagueMembershipsDto, currentUserId: string): LeagueMemberViewModel[] {
|
||||
return dto.members.map(member => new LeagueMemberViewModel(member, currentUserId));
|
||||
}
|
||||
}
|
||||
@@ -1,115 +0,0 @@
|
||||
import type { Race } from '@core/racing/domain/entities/Race';
|
||||
import type { IRaceRepository } from '@core/racing/application/ports/IRaceRepository';
|
||||
import type { IIsDriverRegisteredForRaceQuery } from '@core/racing/application/queries/IIsDriverRegisteredForRaceQuery';
|
||||
import type { IRegisterForRaceUseCase } from '@core/racing/application/use-cases/IRegisterForRaceUseCase';
|
||||
import type { IWithdrawFromRaceUseCase } from '@core/racing/application/use-cases/IWithdrawFromRaceUseCase';
|
||||
|
||||
export interface LeagueScheduleRaceItemViewModel {
|
||||
id: string;
|
||||
leagueId: string;
|
||||
track: string;
|
||||
car: string;
|
||||
sessionType: string;
|
||||
scheduledAt: Date;
|
||||
status: Race['status'];
|
||||
isUpcoming: boolean;
|
||||
isPast: boolean;
|
||||
isRegistered: boolean;
|
||||
}
|
||||
|
||||
export interface LeagueScheduleViewModel {
|
||||
races: LeagueScheduleRaceItemViewModel[];
|
||||
}
|
||||
|
||||
export interface ILeagueSchedulePresenter {
|
||||
loadLeagueSchedule(leagueId: string, driverId: string): Promise<LeagueScheduleViewModel>;
|
||||
registerForRace(raceId: string, leagueId: string, driverId: string): Promise<void>;
|
||||
withdrawFromRace(raceId: string, driverId: string): Promise<void>;
|
||||
}
|
||||
|
||||
export class LeagueSchedulePresenter implements ILeagueSchedulePresenter {
|
||||
constructor(
|
||||
private raceRepository: IRaceRepository,
|
||||
private isDriverRegisteredForRaceQuery: IIsDriverRegisteredForRaceQuery,
|
||||
private registerForRaceUseCase: IRegisterForRaceUseCase,
|
||||
private withdrawFromRaceUseCase: IWithdrawFromRaceUseCase,
|
||||
) {}
|
||||
|
||||
/**
|
||||
* Load league schedule with registration status for a given driver.
|
||||
*/
|
||||
async loadLeagueSchedule(
|
||||
leagueId: string,
|
||||
driverId: string,
|
||||
): Promise<LeagueScheduleViewModel> {
|
||||
const allRaces = await this.raceRepository.findAll();
|
||||
const leagueRaces = allRaces
|
||||
.filter((race) => race.leagueId === leagueId)
|
||||
.sort(
|
||||
(a, b) =>
|
||||
new Date(a.scheduledAt).getTime() - new Date(b.scheduledAt).getTime(),
|
||||
);
|
||||
|
||||
const now = new Date();
|
||||
|
||||
const registrationStates: Record<string, boolean> = {};
|
||||
await Promise.all(
|
||||
leagueRaces.map(async (race) => {
|
||||
const registered = await this.isDriverRegisteredForRaceQuery.execute({
|
||||
raceId: race.id,
|
||||
driverId,
|
||||
});
|
||||
registrationStates[race.id] = registered;
|
||||
}),
|
||||
);
|
||||
|
||||
const races: LeagueScheduleRaceItemViewModel[] = leagueRaces.map((race) => {
|
||||
const raceDate = new Date(race.scheduledAt);
|
||||
const isPast = race.status === 'completed' || raceDate <= now;
|
||||
const isUpcoming = race.status === 'scheduled' && raceDate > now;
|
||||
|
||||
return {
|
||||
id: race.id,
|
||||
leagueId: race.leagueId,
|
||||
track: race.track,
|
||||
car: race.car,
|
||||
sessionType: race.sessionType,
|
||||
scheduledAt: raceDate,
|
||||
status: race.status,
|
||||
isUpcoming,
|
||||
isPast,
|
||||
isRegistered: registrationStates[race.id] ?? false,
|
||||
};
|
||||
});
|
||||
|
||||
return { races };
|
||||
}
|
||||
|
||||
/**
|
||||
* Register the driver for a race.
|
||||
*/
|
||||
async registerForRace(
|
||||
raceId: string,
|
||||
leagueId: string,
|
||||
driverId: string,
|
||||
): Promise<void> {
|
||||
await this.registerForRaceUseCase.execute({
|
||||
raceId,
|
||||
leagueId,
|
||||
driverId,
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Withdraw the driver from a race.
|
||||
*/
|
||||
async withdrawFromRace(
|
||||
raceId: string,
|
||||
driverId: string,
|
||||
): Promise<void> {
|
||||
await this.withdrawFromRaceUseCase.execute({
|
||||
raceId,
|
||||
driverId,
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -1,14 +0,0 @@
|
||||
import type { ILeagueSchedulePreviewPresenter } from '@core/racing/application/presenters/ILeagueSchedulePreviewPresenter';
|
||||
import type { LeagueSchedulePreviewDTO } from '@core/racing/application/dto/LeagueScheduleDTO';
|
||||
|
||||
export class LeagueSchedulePreviewPresenter implements ILeagueSchedulePreviewPresenter {
|
||||
private data: LeagueSchedulePreviewDTO | null = null;
|
||||
|
||||
present(data: LeagueSchedulePreviewDTO): void {
|
||||
this.data = data;
|
||||
}
|
||||
|
||||
getData(): LeagueSchedulePreviewDTO | null {
|
||||
return this.data;
|
||||
}
|
||||
}
|
||||
@@ -1,153 +0,0 @@
|
||||
import type { ChampionshipConfig } from '@core/racing/domain/types/ChampionshipConfig';
|
||||
import type { BonusRule } from '@core/racing/domain/types/BonusRule';
|
||||
import type {
|
||||
ILeagueScoringConfigPresenter,
|
||||
LeagueScoringConfigData,
|
||||
LeagueScoringConfigViewModel,
|
||||
LeagueScoringChampionshipViewModel,
|
||||
} from '@core/racing/application/presenters/ILeagueScoringConfigPresenter';
|
||||
|
||||
export class LeagueScoringConfigPresenter implements ILeagueScoringConfigPresenter {
|
||||
private viewModel: LeagueScoringConfigViewModel | null = null;
|
||||
|
||||
reset(): void {
|
||||
this.viewModel = null;
|
||||
}
|
||||
|
||||
present(data: LeagueScoringConfigData): LeagueScoringConfigViewModel {
|
||||
const championships: LeagueScoringChampionshipViewModel[] =
|
||||
data.championships.map((champ) => this.mapChampionship(champ));
|
||||
|
||||
const dropPolicySummary =
|
||||
data.preset?.dropPolicySummary ??
|
||||
this.deriveDropPolicyDescriptionFromChampionships(data.championships);
|
||||
|
||||
this.viewModel = {
|
||||
leagueId: data.leagueId,
|
||||
seasonId: data.seasonId,
|
||||
gameId: data.gameId,
|
||||
gameName: data.gameName,
|
||||
scoringPresetId: data.scoringPresetId ?? 'custom',
|
||||
scoringPresetName: data.preset?.name ?? 'Custom',
|
||||
dropPolicySummary,
|
||||
championships,
|
||||
};
|
||||
|
||||
return this.viewModel;
|
||||
}
|
||||
|
||||
getViewModel(): LeagueScoringConfigViewModel {
|
||||
if (!this.viewModel) {
|
||||
throw new Error('Presenter has not been called yet');
|
||||
}
|
||||
return this.viewModel;
|
||||
}
|
||||
|
||||
private mapChampionship(championship: ChampionshipConfig): LeagueScoringChampionshipViewModel {
|
||||
const sessionTypes = championship.sessionTypes.map((s) => s.toString());
|
||||
const pointsPreview = this.buildPointsPreview(championship.pointsTableBySessionType);
|
||||
const bonusSummary = this.buildBonusSummary(
|
||||
championship.bonusRulesBySessionType ?? {},
|
||||
);
|
||||
const dropPolicyDescription = this.deriveDropPolicyDescription(
|
||||
championship.dropScorePolicy,
|
||||
);
|
||||
|
||||
return {
|
||||
id: championship.id,
|
||||
name: championship.name,
|
||||
type: championship.type,
|
||||
sessionTypes,
|
||||
pointsPreview,
|
||||
bonusSummary,
|
||||
dropPolicyDescription,
|
||||
};
|
||||
}
|
||||
|
||||
private buildPointsPreview(
|
||||
tables: Record<string, { getPointsForPosition: (position: number) => number }>,
|
||||
): Array<{ sessionType: string; position: number; points: number }> {
|
||||
const preview: Array<{
|
||||
sessionType: string;
|
||||
position: number;
|
||||
points: number;
|
||||
}> = [];
|
||||
|
||||
const maxPositions = 10;
|
||||
|
||||
for (const [sessionType, table] of Object.entries(tables)) {
|
||||
for (let pos = 1; pos <= maxPositions; pos++) {
|
||||
const points = table.getPointsForPosition(pos);
|
||||
if (points && points !== 0) {
|
||||
preview.push({
|
||||
sessionType,
|
||||
position: pos,
|
||||
points,
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return preview;
|
||||
}
|
||||
|
||||
private buildBonusSummary(
|
||||
bonusRulesBySessionType: Record<string, BonusRule[]>,
|
||||
): string[] {
|
||||
const summaries: string[] = [];
|
||||
|
||||
for (const [sessionType, rules] of Object.entries(bonusRulesBySessionType)) {
|
||||
for (const rule of rules) {
|
||||
if (rule.type === 'fastestLap') {
|
||||
const base = `Fastest lap in ${sessionType}`;
|
||||
if (rule.requiresFinishInTopN) {
|
||||
summaries.push(
|
||||
`${base} +${rule.points} points if finishing P${rule.requiresFinishInTopN} or better`,
|
||||
);
|
||||
} else {
|
||||
summaries.push(`${base} +${rule.points} points`);
|
||||
}
|
||||
} else {
|
||||
summaries.push(
|
||||
`${rule.type} bonus in ${sessionType} worth ${rule.points} points`,
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return summaries;
|
||||
}
|
||||
|
||||
private deriveDropPolicyDescriptionFromChampionships(
|
||||
championships: ChampionshipConfig[],
|
||||
): string {
|
||||
const first = championships[0];
|
||||
if (!first) {
|
||||
return 'All results count';
|
||||
}
|
||||
return this.deriveDropPolicyDescription(first.dropScorePolicy);
|
||||
}
|
||||
|
||||
private deriveDropPolicyDescription(policy: {
|
||||
strategy: string;
|
||||
count?: number;
|
||||
dropCount?: number;
|
||||
}): string {
|
||||
if (!policy || policy.strategy === 'none') {
|
||||
return 'All results count';
|
||||
}
|
||||
|
||||
if (policy.strategy === 'bestNResults' && typeof policy.count === 'number') {
|
||||
return `Best ${policy.count} results count towards the championship`;
|
||||
}
|
||||
|
||||
if (
|
||||
policy.strategy === 'dropWorstN' &&
|
||||
typeof policy.dropCount === 'number'
|
||||
) {
|
||||
return `Worst ${policy.dropCount} results are dropped from the championship total`;
|
||||
}
|
||||
|
||||
return 'Custom drop score rules apply';
|
||||
}
|
||||
}
|
||||
@@ -1,29 +0,0 @@
|
||||
import type {
|
||||
ILeagueScoringPresetsPresenter,
|
||||
LeagueScoringPresetsViewModel,
|
||||
LeagueScoringPresetsResultDTO,
|
||||
} from '@core/racing/application/presenters/ILeagueScoringPresetsPresenter';
|
||||
|
||||
export class LeagueScoringPresetsPresenter implements ILeagueScoringPresetsPresenter {
|
||||
private viewModel: LeagueScoringPresetsViewModel | null = null;
|
||||
|
||||
reset(): void {
|
||||
this.viewModel = null;
|
||||
}
|
||||
|
||||
present(dto: LeagueScoringPresetsResultDTO): void {
|
||||
const { presets } = dto;
|
||||
|
||||
this.viewModel = {
|
||||
presets,
|
||||
totalCount: presets.length,
|
||||
};
|
||||
}
|
||||
|
||||
getViewModel(): LeagueScoringPresetsViewModel {
|
||||
if (!this.viewModel) {
|
||||
throw new Error('Presenter has not been called yet');
|
||||
}
|
||||
return this.viewModel;
|
||||
}
|
||||
}
|
||||
@@ -1,18 +0,0 @@
|
||||
import type { LeagueStandingsDto, StandingEntryDto } from '../dtos';
|
||||
import { LeagueStandingsViewModel } from '../view-models';
|
||||
|
||||
/**
|
||||
* League Standings Presenter
|
||||
* Transforms LeagueStandingsDto to LeagueStandingsViewModel
|
||||
*/
|
||||
export class LeagueStandingsPresenter {
|
||||
present(dto: LeagueStandingsDto, currentUserId: string, previousStandings?: StandingEntryDto[]): LeagueStandingsViewModel {
|
||||
return new LeagueStandingsViewModel(dto, currentUserId, previousStandings);
|
||||
}
|
||||
}
|
||||
|
||||
// Legacy functional export for backward compatibility
|
||||
export const presentLeagueStandings = (dto: LeagueStandingsDto, currentUserId: string, previousStandings?: StandingEntryDto[]): LeagueStandingsViewModel => {
|
||||
const presenter = new LeagueStandingsPresenter();
|
||||
return presenter.present(dto, currentUserId, previousStandings);
|
||||
};
|
||||
@@ -1,42 +0,0 @@
|
||||
import type {
|
||||
ILeagueStatsPresenter,
|
||||
LeagueStatsViewModel,
|
||||
} from '@core/racing/application/presenters/ILeagueStatsPresenter';
|
||||
|
||||
export class LeagueStatsPresenter implements ILeagueStatsPresenter {
|
||||
private viewModel: LeagueStatsViewModel | null = null;
|
||||
|
||||
present(
|
||||
leagueId: string,
|
||||
totalRaces: number,
|
||||
completedRaces: number,
|
||||
scheduledRaces: number,
|
||||
sofValues: number[]
|
||||
): LeagueStatsViewModel {
|
||||
const averageSOF = sofValues.length > 0
|
||||
? Math.round(sofValues.reduce((a, b) => a + b, 0) / sofValues.length)
|
||||
: null;
|
||||
|
||||
const highestSOF = sofValues.length > 0 ? Math.max(...sofValues) : null;
|
||||
const lowestSOF = sofValues.length > 0 ? Math.min(...sofValues) : null;
|
||||
|
||||
this.viewModel = {
|
||||
leagueId,
|
||||
totalRaces,
|
||||
completedRaces,
|
||||
scheduledRaces,
|
||||
averageSOF,
|
||||
highestSOF,
|
||||
lowestSOF,
|
||||
};
|
||||
|
||||
return this.viewModel;
|
||||
}
|
||||
|
||||
getViewModel(): LeagueStatsViewModel {
|
||||
if (!this.viewModel) {
|
||||
throw new Error('Presenter has not been called yet');
|
||||
}
|
||||
return this.viewModel;
|
||||
}
|
||||
}
|
||||
@@ -1,21 +0,0 @@
|
||||
import type { AllLeaguesWithCapacityDto } from '../dtos';
|
||||
import { LeagueSummaryViewModel } from '../view-models';
|
||||
|
||||
/**
|
||||
* League Summary Presenter
|
||||
* Transforms AllLeaguesWithCapacityDto to array of LeagueSummaryViewModel
|
||||
*/
|
||||
export class LeagueSummaryPresenter {
|
||||
present(dto: AllLeaguesWithCapacityDto): LeagueSummaryViewModel[] {
|
||||
return dto.leagues.map(league => new LeagueSummaryViewModel(league));
|
||||
}
|
||||
}
|
||||
|
||||
// Legacy functional exports for backward compatibility
|
||||
export const presentLeagueSummary = (dto: any): LeagueSummaryViewModel => {
|
||||
return new LeagueSummaryViewModel(dto);
|
||||
};
|
||||
|
||||
export const presentLeagueSummaries = (dtos: any[]): LeagueSummaryViewModel[] => {
|
||||
return dtos.map(presentLeagueSummary);
|
||||
};
|
||||
@@ -1,35 +0,0 @@
|
||||
import type { GetMediaOutputDto, UploadMediaOutputDto, DeleteMediaOutputDto } from '../dtos';
|
||||
import type { MediaViewModel, UploadMediaViewModel, DeleteMediaViewModel } from '../view-models';
|
||||
|
||||
/**
|
||||
* Media Presenter
|
||||
* Transforms media DTOs to ViewModels
|
||||
*/
|
||||
export class MediaPresenter {
|
||||
presentMedia(dto: GetMediaOutputDto): MediaViewModel {
|
||||
return {
|
||||
id: dto.id,
|
||||
url: dto.url,
|
||||
type: dto.type,
|
||||
category: dto.category,
|
||||
uploadedAt: new Date(dto.uploadedAt),
|
||||
size: dto.size,
|
||||
};
|
||||
}
|
||||
|
||||
presentUpload(dto: UploadMediaOutputDto): UploadMediaViewModel {
|
||||
return {
|
||||
success: dto.success,
|
||||
mediaId: dto.mediaId,
|
||||
url: dto.url,
|
||||
error: dto.error,
|
||||
};
|
||||
}
|
||||
|
||||
presentDelete(dto: DeleteMediaOutputDto): DeleteMediaViewModel {
|
||||
return {
|
||||
success: dto.success,
|
||||
error: dto.error,
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -1,6 +0,0 @@
|
||||
import { MembershipFeeDto } from '../dtos';
|
||||
import { MembershipFeeViewModel } from '../view-models';
|
||||
|
||||
export const presentMembershipFee = (dto: MembershipFeeDto): MembershipFeeViewModel => {
|
||||
return new MembershipFeeViewModel(dto);
|
||||
};
|
||||
@@ -1,17 +0,0 @@
|
||||
import type { GetPaymentsOutputDto } from '../dtos';
|
||||
import { PaymentViewModel } from '../view-models';
|
||||
import { presentPayment } from './PaymentPresenter';
|
||||
|
||||
/**
|
||||
* Payment List Presenter
|
||||
*
|
||||
* Transforms payment list DTOs into ViewModels for UI consumption.
|
||||
*/
|
||||
export class PaymentListPresenter {
|
||||
/**
|
||||
* Transform payment list DTO to ViewModels
|
||||
*/
|
||||
present(dto: GetPaymentsOutputDto): PaymentViewModel[] {
|
||||
return dto.payments.map(payment => presentPayment(payment));
|
||||
}
|
||||
}
|
||||
@@ -1,6 +0,0 @@
|
||||
import { PaymentDto } from '../dtos';
|
||||
import { PaymentViewModel } from '../view-models';
|
||||
|
||||
export const presentPayment = (dto: PaymentDto): PaymentViewModel => {
|
||||
return new PaymentViewModel(dto);
|
||||
};
|
||||
@@ -1,21 +0,0 @@
|
||||
import type {
|
||||
IPendingSponsorshipRequestsPresenter,
|
||||
PendingSponsorshipRequestsViewModel,
|
||||
} from '@core/racing/application/presenters/IPendingSponsorshipRequestsPresenter';
|
||||
import type { GetPendingSponsorshipRequestsResultDTO } from '@core/racing/application/use-cases/GetPendingSponsorshipRequestsUseCase';
|
||||
|
||||
export class PendingSponsorshipRequestsPresenter implements IPendingSponsorshipRequestsPresenter {
|
||||
private viewModel: PendingSponsorshipRequestsViewModel | null = null;
|
||||
|
||||
reset(): void {
|
||||
this.viewModel = null;
|
||||
}
|
||||
|
||||
present(data: GetPendingSponsorshipRequestsResultDTO): void {
|
||||
this.viewModel = data;
|
||||
}
|
||||
|
||||
getViewModel(): PendingSponsorshipRequestsViewModel | null {
|
||||
return this.viewModel;
|
||||
}
|
||||
}
|
||||
@@ -1,6 +0,0 @@
|
||||
import { PrizeDto } from '../dtos';
|
||||
import { PrizeViewModel } from '../view-models';
|
||||
|
||||
export const presentPrize = (dto: PrizeDto): PrizeViewModel => {
|
||||
return new PrizeViewModel(dto);
|
||||
};
|
||||
@@ -1,16 +0,0 @@
|
||||
import type {
|
||||
IProfileOverviewPresenter,
|
||||
ProfileOverviewViewModel,
|
||||
} from '@core/racing/application/presenters/IProfileOverviewPresenter';
|
||||
|
||||
export class ProfileOverviewPresenter implements IProfileOverviewPresenter {
|
||||
private viewModel: ProfileOverviewViewModel | null = null;
|
||||
|
||||
present(viewModel: ProfileOverviewViewModel): void {
|
||||
this.viewModel = viewModel;
|
||||
}
|
||||
|
||||
getViewModel(): ProfileOverviewViewModel | null {
|
||||
return this.viewModel;
|
||||
}
|
||||
}
|
||||
@@ -1,13 +0,0 @@
|
||||
import { RaceDetailDto } from '../dtos';
|
||||
import { RaceDetailViewModel } from '../view-models';
|
||||
|
||||
export class RaceDetailPresenter {
|
||||
present(dto: RaceDetailDto): RaceDetailViewModel {
|
||||
return new RaceDetailViewModel(dto);
|
||||
}
|
||||
}
|
||||
|
||||
export const presentRaceDetail = (dto: RaceDetailDto): RaceDetailViewModel => {
|
||||
const presenter = new RaceDetailPresenter();
|
||||
return presenter.present(dto);
|
||||
};
|
||||
@@ -1,6 +0,0 @@
|
||||
import { RaceListItemDto } from '../dtos';
|
||||
import { RaceListItemViewModel } from '../view-models';
|
||||
|
||||
export const presentRaceListItem = (dto: RaceListItemDto): RaceListItemViewModel => {
|
||||
return new RaceListItemViewModel(dto);
|
||||
};
|
||||
@@ -1,55 +0,0 @@
|
||||
import type {
|
||||
IRacePenaltiesPresenter,
|
||||
RacePenaltyViewModel,
|
||||
RacePenaltiesResultDTO,
|
||||
RacePenaltiesViewModel,
|
||||
} from '@core/racing/application/presenters/IRacePenaltiesPresenter';
|
||||
|
||||
export class RacePenaltiesPresenter implements IRacePenaltiesPresenter {
|
||||
private viewModel: RacePenaltiesViewModel | null = null;
|
||||
|
||||
reset(): void {
|
||||
this.viewModel = null;
|
||||
}
|
||||
|
||||
present(dto: RacePenaltiesResultDTO): void {
|
||||
const { penalties, driverMap } = dto;
|
||||
|
||||
const penaltyViewModels: RacePenaltyViewModel[] = penalties.map((penalty) => {
|
||||
const value = typeof penalty.value === 'number' ? penalty.value : 0;
|
||||
const protestId = penalty.protestId;
|
||||
const appliedAt = penalty.appliedAt ? penalty.appliedAt.toISOString() : undefined;
|
||||
const notes = penalty.notes;
|
||||
|
||||
const base: RacePenaltyViewModel = {
|
||||
id: penalty.id,
|
||||
raceId: penalty.raceId,
|
||||
driverId: penalty.driverId,
|
||||
driverName: driverMap.get(penalty.driverId) || 'Unknown',
|
||||
type: penalty.type,
|
||||
value,
|
||||
reason: penalty.reason,
|
||||
issuedBy: penalty.issuedBy,
|
||||
issuedByName: driverMap.get(penalty.issuedBy) || 'Unknown',
|
||||
status: penalty.status,
|
||||
description: penalty.getDescription(),
|
||||
issuedAt: penalty.issuedAt.toISOString(),
|
||||
};
|
||||
|
||||
return {
|
||||
...base,
|
||||
...(protestId ? { protestId } : {}),
|
||||
...(appliedAt ? { appliedAt } : {}),
|
||||
...(typeof notes === 'string' && notes.length > 0 ? { notes } : {}),
|
||||
};
|
||||
});
|
||||
|
||||
this.viewModel = {
|
||||
penalties: penaltyViewModels,
|
||||
};
|
||||
}
|
||||
|
||||
getViewModel(): RacePenaltiesViewModel | null {
|
||||
return this.viewModel;
|
||||
}
|
||||
}
|
||||
@@ -1,43 +0,0 @@
|
||||
import { RaceDetailViewModel } from '../view-models/RaceDetailViewModel';
|
||||
import type { RaceDetailDto } from '../dtos/RaceDetailDto';
|
||||
import type { RacesPageDataDto } from '../dtos/RacesPageDataDto';
|
||||
import type { RacesPageViewModel } from '../view-models/RacesPageViewModel';
|
||||
|
||||
/**
|
||||
* Race Presenter
|
||||
*
|
||||
* Stateless presenter that transforms race DTOs into view models.
|
||||
* All methods are pure functions with no side effects.
|
||||
*/
|
||||
export class RacePresenter {
|
||||
presentRaceDetail(dto: RaceDetailDto): RaceDetailViewModel {
|
||||
return new RaceDetailViewModel(dto);
|
||||
}
|
||||
|
||||
presentRacesPage(dto: RacesPageDataDto): RacesPageViewModel {
|
||||
return {
|
||||
upcomingRaces: dto.races.filter(r => r.isUpcoming).map(r => this.presentRaceCard(r)),
|
||||
completedRaces: dto.races.filter(r => r.status === 'completed').map(r => this.presentRaceCard(r)),
|
||||
totalCount: dto.races.length,
|
||||
};
|
||||
}
|
||||
|
||||
private presentRaceCard(race: any): any {
|
||||
return {
|
||||
id: race.id,
|
||||
title: race.title || race.track,
|
||||
scheduledTime: race.scheduledTime || race.scheduledAt,
|
||||
status: this.formatStatus(race.status),
|
||||
};
|
||||
}
|
||||
|
||||
private formatStatus(status: string): string {
|
||||
const statusMap: Record<string, string> = {
|
||||
scheduled: 'Scheduled',
|
||||
running: 'Live',
|
||||
completed: 'Finished',
|
||||
cancelled: 'Cancelled',
|
||||
};
|
||||
return statusMap[status] || status;
|
||||
}
|
||||
}
|
||||
@@ -1,60 +0,0 @@
|
||||
import type {
|
||||
IRaceProtestsPresenter,
|
||||
RaceProtestViewModel,
|
||||
RaceProtestsResultDTO,
|
||||
RaceProtestsViewModel,
|
||||
} from '@core/racing/application/presenters/IRaceProtestsPresenter';
|
||||
|
||||
export class RaceProtestsPresenter implements IRaceProtestsPresenter {
|
||||
private viewModel: RaceProtestsViewModel | null = null;
|
||||
|
||||
reset(): void {
|
||||
this.viewModel = null;
|
||||
}
|
||||
|
||||
present(dto: RaceProtestsResultDTO): void {
|
||||
const { protests, driverMap } = dto;
|
||||
|
||||
const protestViewModels: RaceProtestViewModel[] = protests.map((protest) => {
|
||||
const base: RaceProtestViewModel = {
|
||||
id: protest.id,
|
||||
raceId: protest.raceId,
|
||||
protestingDriverId: protest.protestingDriverId,
|
||||
protestingDriverName: driverMap.get(protest.protestingDriverId) || 'Unknown',
|
||||
accusedDriverId: protest.accusedDriverId,
|
||||
accusedDriverName: driverMap.get(protest.accusedDriverId) || 'Unknown',
|
||||
incident: protest.incident,
|
||||
filedAt: protest.filedAt.toISOString(),
|
||||
status: protest.status,
|
||||
};
|
||||
|
||||
const comment = protest.comment;
|
||||
const proofVideoUrl = protest.proofVideoUrl;
|
||||
const reviewedBy = protest.reviewedBy;
|
||||
const reviewedByName =
|
||||
protest.reviewedBy !== undefined
|
||||
? driverMap.get(protest.reviewedBy) ?? 'Unknown'
|
||||
: undefined;
|
||||
const decisionNotes = protest.decisionNotes;
|
||||
const reviewedAt = protest.reviewedAt?.toISOString();
|
||||
|
||||
return {
|
||||
...base,
|
||||
...(comment !== undefined ? { comment } : {}),
|
||||
...(proofVideoUrl !== undefined ? { proofVideoUrl } : {}),
|
||||
...(reviewedBy !== undefined ? { reviewedBy } : {}),
|
||||
...(reviewedByName !== undefined ? { reviewedByName } : {}),
|
||||
...(decisionNotes !== undefined ? { decisionNotes } : {}),
|
||||
...(reviewedAt !== undefined ? { reviewedAt } : {}),
|
||||
};
|
||||
});
|
||||
|
||||
this.viewModel = {
|
||||
protests: protestViewModels,
|
||||
};
|
||||
}
|
||||
|
||||
getViewModel(): RaceProtestsViewModel | null {
|
||||
return this.viewModel;
|
||||
}
|
||||
}
|
||||
@@ -1,25 +0,0 @@
|
||||
import type {
|
||||
IRaceRegistrationsPresenter,
|
||||
RaceRegistrationsViewModel,
|
||||
RaceRegistrationsResultDTO,
|
||||
} from '@core/racing/application/presenters/IRaceRegistrationsPresenter';
|
||||
|
||||
export class RaceRegistrationsPresenter implements IRaceRegistrationsPresenter {
|
||||
private viewModel: RaceRegistrationsViewModel | null = null;
|
||||
|
||||
reset(): void {
|
||||
this.viewModel = null;
|
||||
}
|
||||
|
||||
present(input: RaceRegistrationsResultDTO): void {
|
||||
const { registeredDriverIds } = input;
|
||||
this.viewModel = {
|
||||
registeredDriverIds,
|
||||
count: registeredDriverIds.length,
|
||||
};
|
||||
}
|
||||
|
||||
getViewModel(): RaceRegistrationsViewModel | null {
|
||||
return this.viewModel;
|
||||
}
|
||||
}
|
||||
@@ -1,13 +0,0 @@
|
||||
import { RaceResultsDetailDto } from '../dtos';
|
||||
import { RaceResultsDetailViewModel } from '../view-models';
|
||||
|
||||
export class RaceResultsDetailPresenter {
|
||||
present(dto: RaceResultsDetailDto, currentUserId?: string): RaceResultsDetailViewModel {
|
||||
return new RaceResultsDetailViewModel(dto, currentUserId);
|
||||
}
|
||||
}
|
||||
|
||||
export const presentRaceResultsDetail = (dto: RaceResultsDetailDto, currentUserId?: string): RaceResultsDetailViewModel => {
|
||||
const presenter = new RaceResultsDetailPresenter();
|
||||
return presenter.present(dto, currentUserId);
|
||||
};
|
||||
@@ -1,52 +0,0 @@
|
||||
import { describe, it, expect } from 'vitest';
|
||||
import { presentRaceResult } from './RaceResultsPresenter';
|
||||
import { RaceResultViewModel } from '../view-models';
|
||||
import type { RaceResultRowDto } from '../dtos';
|
||||
|
||||
describe('RaceResultsPresenter', () => {
|
||||
describe('presentRaceResult', () => {
|
||||
it('should transform RaceResultRowDto into RaceResultViewModel', () => {
|
||||
const dto: RaceResultRowDto = {
|
||||
id: '1',
|
||||
raceId: 'race-1',
|
||||
driverId: 'driver-1',
|
||||
position: 1,
|
||||
fastestLap: 85.5,
|
||||
incidents: 0,
|
||||
startPosition: 2,
|
||||
};
|
||||
|
||||
const result = presentRaceResult(dto);
|
||||
|
||||
expect(result).toBeInstanceOf(RaceResultViewModel);
|
||||
expect(result.id).toBe('1');
|
||||
expect(result.raceId).toBe('race-1');
|
||||
expect(result.driverId).toBe('driver-1');
|
||||
expect(result.position).toBe(1);
|
||||
expect(result.fastestLap).toBe(85.5);
|
||||
expect(result.incidents).toBe(0);
|
||||
expect(result.startPosition).toBe(2);
|
||||
});
|
||||
|
||||
it('should handle zero values correctly', () => {
|
||||
const dto: RaceResultRowDto = {
|
||||
id: '2',
|
||||
raceId: 'race-2',
|
||||
driverId: 'driver-2',
|
||||
position: 5,
|
||||
fastestLap: 0,
|
||||
incidents: 3,
|
||||
startPosition: 5,
|
||||
};
|
||||
|
||||
const result = presentRaceResult(dto);
|
||||
|
||||
expect(result).toBeInstanceOf(RaceResultViewModel);
|
||||
expect(result.id).toBe('2');
|
||||
expect(result.position).toBe(5);
|
||||
expect(result.fastestLap).toBe(0);
|
||||
expect(result.incidents).toBe(3);
|
||||
expect(result.startPosition).toBe(5);
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -1,6 +0,0 @@
|
||||
import { RaceResultRowDto } from '../dtos';
|
||||
import { RaceResultViewModel } from '../view-models';
|
||||
|
||||
export const presentRaceResult = (dto: RaceResultRowDto): RaceResultViewModel => {
|
||||
return new RaceResultViewModel(dto);
|
||||
};
|
||||
@@ -1,17 +0,0 @@
|
||||
import type { RaceWithSOFDto } from '../dtos/RaceWithSOFDto';
|
||||
|
||||
export interface RaceWithSOFViewModel {
|
||||
id: string;
|
||||
track: string;
|
||||
strengthOfField: number | null;
|
||||
}
|
||||
|
||||
export class RaceWithSOFPresenter {
|
||||
present(dto: RaceWithSOFDto): RaceWithSOFViewModel {
|
||||
return {
|
||||
id: dto.id,
|
||||
track: dto.track,
|
||||
strengthOfField: dto.strengthOfField,
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -1,89 +0,0 @@
|
||||
import type {
|
||||
IRacesPagePresenter,
|
||||
RacesPageViewModel,
|
||||
RaceListItemViewModel,
|
||||
RacesPageResultDTO,
|
||||
} from '@core/racing/application/presenters/IRacesPagePresenter';
|
||||
|
||||
export class RacesPagePresenter implements IRacesPagePresenter {
|
||||
private viewModel: RacesPageViewModel | null = null;
|
||||
|
||||
reset(): void {
|
||||
this.viewModel = null;
|
||||
}
|
||||
|
||||
present(input: RacesPageResultDTO): void {
|
||||
const { races } = input;
|
||||
const now = new Date();
|
||||
const nextWeek = new Date(now.getTime() + 7 * 24 * 60 * 60 * 1000);
|
||||
|
||||
const raceViewModels: RaceListItemViewModel[] = races.map((race) => {
|
||||
const scheduledAt =
|
||||
typeof race.scheduledAt === 'string'
|
||||
? race.scheduledAt
|
||||
: race.scheduledAt.toISOString();
|
||||
|
||||
const allowedStatuses: RaceListItemViewModel['status'][] = [
|
||||
'scheduled',
|
||||
'running',
|
||||
'completed',
|
||||
'cancelled',
|
||||
];
|
||||
|
||||
const status: RaceListItemViewModel['status'] =
|
||||
allowedStatuses.includes(race.status as RaceListItemViewModel['status'])
|
||||
? (race.status as RaceListItemViewModel['status'])
|
||||
: 'scheduled';
|
||||
|
||||
return {
|
||||
id: race.id,
|
||||
track: race.track,
|
||||
car: race.car,
|
||||
scheduledAt,
|
||||
status,
|
||||
leagueId: race.leagueId,
|
||||
leagueName: race.leagueName,
|
||||
strengthOfField: race.strengthOfField,
|
||||
isUpcoming: race.isUpcoming,
|
||||
isLive: race.isLive,
|
||||
isPast: race.isPast,
|
||||
};
|
||||
});
|
||||
|
||||
const stats = {
|
||||
total: raceViewModels.length,
|
||||
scheduled: raceViewModels.filter(r => r.status === 'scheduled').length,
|
||||
running: raceViewModels.filter(r => r.status === 'running').length,
|
||||
completed: raceViewModels.filter(r => r.status === 'completed').length,
|
||||
};
|
||||
|
||||
const liveRaces = raceViewModels.filter(r => r.isLive);
|
||||
|
||||
const upcomingThisWeek = raceViewModels
|
||||
.filter(r => {
|
||||
const scheduledDate = new Date(r.scheduledAt);
|
||||
return r.isUpcoming && scheduledDate >= now && scheduledDate <= nextWeek;
|
||||
})
|
||||
.slice(0, 5);
|
||||
|
||||
const recentResults = raceViewModels
|
||||
.filter(r => r.status === 'completed')
|
||||
.sort((a, b) => new Date(b.scheduledAt).getTime() - new Date(a.scheduledAt).getTime())
|
||||
.slice(0, 3);
|
||||
|
||||
this.viewModel = {
|
||||
races: raceViewModels,
|
||||
stats,
|
||||
liveRaces,
|
||||
upcomingThisWeek,
|
||||
recentResults,
|
||||
};
|
||||
}
|
||||
|
||||
getViewModel(): RacesPageViewModel {
|
||||
if (!this.viewModel) {
|
||||
throw new Error('ViewModel not yet generated. Call present() first.');
|
||||
}
|
||||
return this.viewModel;
|
||||
}
|
||||
}
|
||||
@@ -1,70 +0,0 @@
|
||||
/**
|
||||
* ScheduleRaceFormPresenter - Pure data transformer
|
||||
* Transforms API response to view model without DI dependencies.
|
||||
*/
|
||||
|
||||
import { apiClient } from '@/lib/apiClient';
|
||||
|
||||
export type SessionType = 'practice' | 'qualifying' | 'race';
|
||||
|
||||
export interface ScheduleRaceFormData {
|
||||
leagueId: string;
|
||||
track: string;
|
||||
car: string;
|
||||
sessionType: SessionType;
|
||||
scheduledDate: string;
|
||||
scheduledTime: string;
|
||||
}
|
||||
|
||||
export interface ScheduledRaceViewModel {
|
||||
id: string;
|
||||
leagueId: string;
|
||||
track: string;
|
||||
car: string;
|
||||
sessionType: SessionType;
|
||||
scheduledAt: Date;
|
||||
status: string;
|
||||
}
|
||||
|
||||
export interface LeagueOptionViewModel {
|
||||
id: string;
|
||||
name: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* Load available leagues for the schedule form.
|
||||
*/
|
||||
export async function loadScheduleRaceFormLeagues(): Promise<LeagueOptionViewModel[]> {
|
||||
const response = await apiClient.leagues.getAllWithCapacity();
|
||||
return response.leagues.map((league) => ({
|
||||
id: league.id,
|
||||
name: league.name,
|
||||
}));
|
||||
}
|
||||
|
||||
/**
|
||||
* Schedule a race via API.
|
||||
* Note: This would need a dedicated API endpoint for race scheduling.
|
||||
* For now, this is a placeholder that shows the expected interface.
|
||||
*/
|
||||
export async function scheduleRaceFromForm(
|
||||
formData: ScheduleRaceFormData
|
||||
): Promise<ScheduledRaceViewModel> {
|
||||
const scheduledAt = new Date(`${formData.scheduledDate}T${formData.scheduledTime}`);
|
||||
|
||||
// In the new architecture, race scheduling should be done via API
|
||||
// This is a placeholder that returns expected data structure
|
||||
// The API endpoint would need to be implemented: POST /races
|
||||
|
||||
// For now, return a mock response
|
||||
// TODO: Replace with actual API call when race creation endpoint is available
|
||||
return {
|
||||
id: `race-${Date.now()}`,
|
||||
leagueId: formData.leagueId,
|
||||
track: formData.track.trim(),
|
||||
car: formData.car.trim(),
|
||||
sessionType: formData.sessionType,
|
||||
scheduledAt,
|
||||
status: 'scheduled',
|
||||
};
|
||||
}
|
||||
@@ -1,13 +0,0 @@
|
||||
import type { SessionDataDto } from '../dtos';
|
||||
import { SessionViewModel } from '../view-models';
|
||||
|
||||
/**
|
||||
* Session Presenter
|
||||
* Transforms session DTOs to ViewModels
|
||||
*/
|
||||
export class SessionPresenter {
|
||||
presentSession(dto: SessionDataDto | null): SessionViewModel | null {
|
||||
if (!dto) return null;
|
||||
return new SessionViewModel(dto);
|
||||
}
|
||||
}
|
||||
@@ -1,13 +0,0 @@
|
||||
import type { SponsorDashboardDto } from '../dtos';
|
||||
import { SponsorDashboardViewModel } from '../view-models/SponsorDashboardViewModel';
|
||||
|
||||
/**
|
||||
* Sponsor Dashboard Presenter
|
||||
*
|
||||
* Transforms sponsor dashboard DTOs into view models.
|
||||
*/
|
||||
export class SponsorDashboardPresenter {
|
||||
present(dto: SponsorDashboardDto): SponsorDashboardViewModel {
|
||||
return new SponsorDashboardViewModel(dto);
|
||||
}
|
||||
}
|
||||
@@ -1,13 +0,0 @@
|
||||
import type { GetSponsorsOutputDto } from '../dtos';
|
||||
import { SponsorViewModel } from '../view-models';
|
||||
|
||||
/**
|
||||
* Sponsor List Presenter
|
||||
*
|
||||
* Transforms sponsor list DTOs into view models.
|
||||
*/
|
||||
export class SponsorListPresenter {
|
||||
present(dto: GetSponsorsOutputDto): SponsorViewModel[] {
|
||||
return dto.sponsors.map(sponsor => new SponsorViewModel(sponsor));
|
||||
}
|
||||
}
|
||||
@@ -1,13 +0,0 @@
|
||||
import type { SponsorDto } from '../dtos';
|
||||
import { SponsorViewModel } from '../view-models';
|
||||
|
||||
/**
|
||||
* Sponsor Presenter
|
||||
*
|
||||
* Transforms sponsor DTOs into view models.
|
||||
*/
|
||||
export class SponsorPresenter {
|
||||
present(dto: SponsorDto): SponsorViewModel {
|
||||
return new SponsorViewModel(dto);
|
||||
}
|
||||
}
|
||||
@@ -1,13 +0,0 @@
|
||||
import type { SponsorSponsorshipsDto } from '../dtos';
|
||||
import { SponsorSponsorshipsViewModel } from '../view-models/SponsorSponsorshipsViewModel';
|
||||
|
||||
/**
|
||||
* Sponsor Sponsorships Presenter
|
||||
*
|
||||
* Transforms sponsor sponsorships DTOs into view models.
|
||||
*/
|
||||
export class SponsorSponsorshipsPresenter {
|
||||
present(dto: SponsorSponsorshipsDto): SponsorSponsorshipsViewModel {
|
||||
return new SponsorSponsorshipsViewModel(dto);
|
||||
}
|
||||
}
|
||||
@@ -1,6 +0,0 @@
|
||||
import { SponsorshipDetailDto } from '../dtos';
|
||||
import { SponsorshipDetailViewModel } from '../view-models';
|
||||
|
||||
export const presentSponsorshipDetail = (dto: SponsorshipDetailDto): SponsorshipDetailViewModel => {
|
||||
return new SponsorshipDetailViewModel(dto);
|
||||
};
|
||||
@@ -1,13 +0,0 @@
|
||||
import type { GetEntitySponsorshipPricingResultDto } from '../dtos';
|
||||
import { SponsorshipPricingViewModel } from '../view-models/SponsorshipPricingViewModel';
|
||||
|
||||
/**
|
||||
* Sponsorship Pricing Presenter
|
||||
*
|
||||
* Transforms sponsorship pricing DTOs into view models.
|
||||
*/
|
||||
export class SponsorshipPricingPresenter {
|
||||
present(dto: GetEntitySponsorshipPricingResultDto): SponsorshipPricingViewModel {
|
||||
return new SponsorshipPricingViewModel(dto);
|
||||
}
|
||||
}
|
||||
@@ -1,119 +0,0 @@
|
||||
/**
|
||||
* TeamAdminPresenter - Pure data transformer
|
||||
* Transforms API responses to view models without DI dependencies.
|
||||
* All data fetching is done via apiClient.
|
||||
*/
|
||||
|
||||
import { apiClient } from '@/lib/apiClient';
|
||||
import type { DriverDTO } from '@/lib/apiClient';
|
||||
|
||||
// ============================================================================
|
||||
// View Model Types
|
||||
// ============================================================================
|
||||
|
||||
export interface TeamAdminJoinRequestViewModel {
|
||||
id: string;
|
||||
teamId: string;
|
||||
driverId: string;
|
||||
requestedAt: Date;
|
||||
message?: string | undefined;
|
||||
driver?: DriverDTO | undefined;
|
||||
}
|
||||
|
||||
export interface TeamAdminTeamSummaryViewModel {
|
||||
id: string;
|
||||
name: string;
|
||||
tag: string;
|
||||
description: string;
|
||||
ownerId: string;
|
||||
}
|
||||
|
||||
export interface TeamAdminViewModel {
|
||||
team: TeamAdminTeamSummaryViewModel;
|
||||
requests: TeamAdminJoinRequestViewModel[];
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// Data Fetching Functions (using apiClient)
|
||||
// ============================================================================
|
||||
|
||||
/**
|
||||
* Load team admin view model via API.
|
||||
*/
|
||||
export async function loadTeamAdminViewModel(
|
||||
team: TeamAdminTeamSummaryViewModel
|
||||
): Promise<TeamAdminViewModel> {
|
||||
const requests = await loadTeamJoinRequests(team.id);
|
||||
return {
|
||||
team: {
|
||||
id: team.id,
|
||||
name: team.name,
|
||||
tag: team.tag,
|
||||
description: team.description,
|
||||
ownerId: team.ownerId,
|
||||
},
|
||||
requests,
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Load join requests for a team via API.
|
||||
*/
|
||||
export async function loadTeamJoinRequests(
|
||||
teamId: string
|
||||
): Promise<TeamAdminJoinRequestViewModel[]> {
|
||||
const response = await apiClient.teams.getJoinRequests(teamId);
|
||||
|
||||
return response.requests.map((req) => {
|
||||
const viewModel: TeamAdminJoinRequestViewModel = {
|
||||
id: req.id,
|
||||
teamId: req.teamId,
|
||||
driverId: req.driverId,
|
||||
requestedAt: new Date(req.requestedAt),
|
||||
};
|
||||
|
||||
if (req.message) {
|
||||
viewModel.message = req.message;
|
||||
}
|
||||
|
||||
return viewModel;
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Approve a team join request and return updated request view models.
|
||||
*/
|
||||
export async function approveTeamJoinRequestAndReload(
|
||||
requestId: string,
|
||||
teamId: string
|
||||
): Promise<TeamAdminJoinRequestViewModel[]> {
|
||||
await apiClient.teams.approveJoinRequest(teamId, requestId);
|
||||
return loadTeamJoinRequests(teamId);
|
||||
}
|
||||
|
||||
/**
|
||||
* Reject a team join request and return updated request view models.
|
||||
*/
|
||||
export async function rejectTeamJoinRequestAndReload(
|
||||
requestId: string,
|
||||
teamId: string
|
||||
): Promise<TeamAdminJoinRequestViewModel[]> {
|
||||
await apiClient.teams.rejectJoinRequest(teamId, requestId);
|
||||
return loadTeamJoinRequests(teamId);
|
||||
}
|
||||
|
||||
/**
|
||||
* Update team basic details via API.
|
||||
*/
|
||||
export async function updateTeamDetails(params: {
|
||||
teamId: string;
|
||||
name: string;
|
||||
tag: string;
|
||||
description: string;
|
||||
updatedByDriverId: string;
|
||||
}): Promise<void> {
|
||||
await apiClient.teams.update(params.teamId, {
|
||||
name: params.name,
|
||||
description: params.description,
|
||||
});
|
||||
}
|
||||
@@ -1,12 +0,0 @@
|
||||
import type { TeamDetailsDto } from '../dtos';
|
||||
import { TeamDetailsViewModel } from '../view-models';
|
||||
|
||||
/**
|
||||
* Team Details Presenter
|
||||
* Transforms TeamDetailsDto to TeamDetailsViewModel
|
||||
*/
|
||||
export class TeamDetailsPresenter {
|
||||
present(dto: TeamDetailsDto, currentUserId: string): TeamDetailsViewModel {
|
||||
return new TeamDetailsViewModel(dto, currentUserId);
|
||||
}
|
||||
}
|
||||
@@ -1,18 +0,0 @@
|
||||
import type { TeamJoinRequestItemDto } from '../dtos';
|
||||
import { TeamJoinRequestViewModel } from '../view-models';
|
||||
|
||||
/**
|
||||
* Team Join Request Presenter
|
||||
* Transforms TeamJoinRequestItemDto to TeamJoinRequestViewModel
|
||||
*/
|
||||
export class TeamJoinRequestPresenter {
|
||||
present(dto: TeamJoinRequestItemDto, currentUserId: string, isOwner: boolean): TeamJoinRequestViewModel {
|
||||
return new TeamJoinRequestViewModel(dto, currentUserId, isOwner);
|
||||
}
|
||||
}
|
||||
|
||||
// Backward compatibility export (deprecated)
|
||||
export const presentTeamJoinRequest = (dto: TeamJoinRequestItemDto, currentUserId: string, isOwner: boolean): TeamJoinRequestViewModel => {
|
||||
const presenter = new TeamJoinRequestPresenter();
|
||||
return presenter.present(dto, currentUserId, isOwner);
|
||||
};
|
||||
@@ -1,38 +0,0 @@
|
||||
import type {
|
||||
ITeamJoinRequestsPresenter,
|
||||
TeamJoinRequestViewModel,
|
||||
TeamJoinRequestsViewModel,
|
||||
TeamJoinRequestsResultDTO,
|
||||
} from '@core/racing/application/presenters/ITeamJoinRequestsPresenter';
|
||||
|
||||
export class TeamJoinRequestsPresenter implements ITeamJoinRequestsPresenter {
|
||||
private viewModel: TeamJoinRequestsViewModel | null = null;
|
||||
|
||||
reset(): void {
|
||||
this.viewModel = null;
|
||||
}
|
||||
|
||||
present(input: TeamJoinRequestsResultDTO): void {
|
||||
const requestItems: TeamJoinRequestViewModel[] = input.requests.map((request) => ({
|
||||
requestId: request.id,
|
||||
driverId: request.driverId,
|
||||
driverName: input.driverNames[request.driverId] ?? 'Unknown Driver',
|
||||
teamId: request.teamId,
|
||||
status: 'pending',
|
||||
requestedAt: request.requestedAt.toISOString(),
|
||||
avatarUrl: input.avatarUrls[request.driverId] ?? '',
|
||||
}));
|
||||
|
||||
const pendingCount = requestItems.filter((r) => r.status === 'pending').length;
|
||||
|
||||
this.viewModel = {
|
||||
requests: requestItems,
|
||||
pendingCount,
|
||||
totalCount: requestItems.length,
|
||||
};
|
||||
}
|
||||
|
||||
getViewModel(): TeamJoinRequestsViewModel | null {
|
||||
return this.viewModel;
|
||||
}
|
||||
}
|
||||
@@ -1,12 +0,0 @@
|
||||
import type { AllTeamsDto } from '../dtos';
|
||||
import { TeamSummaryViewModel } from '../view-models';
|
||||
|
||||
/**
|
||||
* Team List Presenter
|
||||
* Transforms AllTeamsDto to array of TeamSummaryViewModel
|
||||
*/
|
||||
export class TeamListPresenter {
|
||||
present(dto: AllTeamsDto): TeamSummaryViewModel[] {
|
||||
return dto.teams.map(team => new TeamSummaryViewModel(team));
|
||||
}
|
||||
}
|
||||
@@ -1,6 +0,0 @@
|
||||
import { TeamMemberDto } from '../dtos';
|
||||
import { TeamMemberViewModel } from '../view-models';
|
||||
|
||||
export const presentTeamMember = (dto: TeamMemberDto, currentUserId: string, teamOwnerId: string): TeamMemberViewModel => {
|
||||
return new TeamMemberViewModel(dto, currentUserId, teamOwnerId);
|
||||
};
|
||||
@@ -1,12 +0,0 @@
|
||||
import type { TeamMembersDto } from '../dtos';
|
||||
import { TeamMemberViewModel } from '../view-models';
|
||||
|
||||
/**
|
||||
* Team Members Presenter
|
||||
* Transforms TeamMembersDto to array of TeamMemberViewModel
|
||||
*/
|
||||
export class TeamMembersPresenter {
|
||||
present(dto: TeamMembersDto, currentUserId: string, teamOwnerId: string): TeamMemberViewModel[] {
|
||||
return dto.members.map(member => new TeamMemberViewModel(member, currentUserId, teamOwnerId));
|
||||
}
|
||||
}
|
||||
@@ -1,61 +0,0 @@
|
||||
/**
|
||||
* TeamRosterPresenter - Pure data transformer
|
||||
* Transforms API response to view model without DI dependencies.
|
||||
*/
|
||||
|
||||
import { apiClient } from '@/lib/apiClient';
|
||||
|
||||
export type TeamRole = 'owner' | 'manager' | 'driver' | 'member';
|
||||
|
||||
export interface DriverDTO {
|
||||
id: string;
|
||||
name: string;
|
||||
avatarUrl?: string | undefined;
|
||||
country?: string | undefined;
|
||||
}
|
||||
|
||||
export interface TeamRosterMemberViewModel {
|
||||
driver: DriverDTO;
|
||||
role: TeamRole;
|
||||
joinedAt: string;
|
||||
rating: number | null;
|
||||
overallRank: number | null;
|
||||
}
|
||||
|
||||
export interface TeamRosterViewModel {
|
||||
members: TeamRosterMemberViewModel[];
|
||||
averageRating: number;
|
||||
}
|
||||
|
||||
/**
|
||||
* Fetch team roster via API and transform to view model.
|
||||
*/
|
||||
export async function getTeamRosterViewModel(
|
||||
teamId: string
|
||||
): Promise<TeamRosterViewModel> {
|
||||
const response = await apiClient.teams.getMembers(teamId);
|
||||
|
||||
const members: TeamRosterMemberViewModel[] = response.members.map((member) => ({
|
||||
driver: {
|
||||
id: member.driverId,
|
||||
name: member.driver?.name ?? 'Unknown',
|
||||
avatarUrl: member.driver?.avatarUrl,
|
||||
},
|
||||
role: (member.role as TeamRole) ?? 'member',
|
||||
joinedAt: member.joinedAt,
|
||||
rating: null, // Would need from API
|
||||
overallRank: null, // Would need from API
|
||||
}));
|
||||
|
||||
const averageRating =
|
||||
members.length > 0
|
||||
? Math.round(
|
||||
members.reduce((sum, m) => sum + (m.rating ?? 0), 0) / members.length
|
||||
)
|
||||
: 0;
|
||||
|
||||
return {
|
||||
members,
|
||||
averageRating,
|
||||
};
|
||||
}
|
||||
@@ -1,57 +0,0 @@
|
||||
/**
|
||||
* TeamStandingsPresenter - Pure data transformer
|
||||
* Transforms API response to view model without DI dependencies.
|
||||
*/
|
||||
|
||||
import { apiClient } from '@/lib/apiClient';
|
||||
|
||||
export interface TeamLeagueStandingViewModel {
|
||||
leagueId: string;
|
||||
leagueName: string;
|
||||
position: number;
|
||||
points: number;
|
||||
wins: number;
|
||||
racesCompleted: number;
|
||||
}
|
||||
|
||||
export interface TeamStandingsViewModel {
|
||||
standings: TeamLeagueStandingViewModel[];
|
||||
}
|
||||
|
||||
/**
|
||||
* Compute team standings across the given leagues for a team.
|
||||
* This would need a dedicated API endpoint for team standings.
|
||||
* For now, returns empty standings - the API should provide this data.
|
||||
* @param teamId - The team ID (will be used when API supports team standings)
|
||||
* @param leagueIds - List of league IDs to fetch standings for
|
||||
*/
|
||||
export async function loadTeamStandings(
|
||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||
teamId: string,
|
||||
leagueIds: string[],
|
||||
): Promise<TeamStandingsViewModel> {
|
||||
// In the new architecture, team standings should come from API
|
||||
// For now, fetch each league's standings and aggregate
|
||||
const teamStandings: TeamLeagueStandingViewModel[] = [];
|
||||
|
||||
for (const leagueId of leagueIds) {
|
||||
try {
|
||||
const standings = await apiClient.leagues.getStandings(leagueId);
|
||||
|
||||
// Since we don't have team-specific standings from API yet,
|
||||
// this is a placeholder that returns basic data
|
||||
teamStandings.push({
|
||||
leagueId,
|
||||
leagueName: `League ${leagueId}`, // Would need from API
|
||||
position: 0,
|
||||
points: 0,
|
||||
wins: 0,
|
||||
racesCompleted: standings.standings.length > 0 ? 1 : 0,
|
||||
});
|
||||
} catch {
|
||||
// Skip leagues that fail to load
|
||||
}
|
||||
}
|
||||
|
||||
return { standings: teamStandings };
|
||||
}
|
||||
@@ -1,6 +0,0 @@
|
||||
import { TeamSummaryDto } from '../dtos';
|
||||
import { TeamSummaryViewModel } from '../view-models';
|
||||
|
||||
export const presentTeamSummary = (dto: TeamSummaryDto): TeamSummaryViewModel => {
|
||||
return new TeamSummaryViewModel(dto);
|
||||
};
|
||||
@@ -1,100 +0,0 @@
|
||||
import type {
|
||||
ITeamsLeaderboardPresenter,
|
||||
TeamsLeaderboardViewModel,
|
||||
TeamLeaderboardItemViewModel,
|
||||
SkillLevel,
|
||||
TeamsLeaderboardResultDTO,
|
||||
} from '@core/racing/application/presenters/ITeamsLeaderboardPresenter';
|
||||
|
||||
interface TeamLeaderboardInput {
|
||||
id: string;
|
||||
name: string;
|
||||
memberCount: number;
|
||||
rating: number | null;
|
||||
totalWins: number;
|
||||
totalRaces: number;
|
||||
performanceLevel: SkillLevel;
|
||||
isRecruiting: boolean;
|
||||
createdAt: Date;
|
||||
description?: string | null;
|
||||
specialization?: string | null;
|
||||
region?: string | null;
|
||||
languages?: string[];
|
||||
}
|
||||
|
||||
export class TeamsLeaderboardPresenter implements ITeamsLeaderboardPresenter {
|
||||
private viewModel: TeamsLeaderboardViewModel | null = null;
|
||||
|
||||
reset(): void {
|
||||
this.viewModel = null;
|
||||
}
|
||||
|
||||
present(input: TeamsLeaderboardResultDTO): void {
|
||||
const teams = (input.teams ?? []) as TeamLeaderboardInput[];
|
||||
const recruitingCount = input.recruitingCount ?? 0;
|
||||
|
||||
const transformedTeams = teams.map((team) => this.transformTeam(team));
|
||||
|
||||
const groupsBySkillLevel = transformedTeams.reduce<Record<SkillLevel, TeamLeaderboardItemViewModel[]>>(
|
||||
(acc, team) => {
|
||||
if (!acc[team.performanceLevel]) {
|
||||
acc[team.performanceLevel] = [];
|
||||
}
|
||||
acc[team.performanceLevel]!.push(team);
|
||||
return acc;
|
||||
},
|
||||
{
|
||||
beginner: [],
|
||||
intermediate: [],
|
||||
advanced: [],
|
||||
pro: [],
|
||||
},
|
||||
);
|
||||
|
||||
const topTeams = transformedTeams
|
||||
.filter((t) => t.rating !== null)
|
||||
.slice()
|
||||
.sort((a, b) => (b.rating ?? 0) - (a.rating ?? 0))
|
||||
.slice(0, 5);
|
||||
|
||||
this.viewModel = {
|
||||
teams: transformedTeams,
|
||||
recruitingCount,
|
||||
groupsBySkillLevel,
|
||||
topTeams,
|
||||
};
|
||||
}
|
||||
|
||||
getViewModel(): TeamsLeaderboardViewModel | null {
|
||||
return this.viewModel;
|
||||
}
|
||||
|
||||
private transformTeam(team: TeamLeaderboardInput): TeamLeaderboardItemViewModel {
|
||||
let specialization: TeamLeaderboardItemViewModel['specialization'];
|
||||
if (
|
||||
team.specialization === 'endurance' ||
|
||||
team.specialization === 'sprint' ||
|
||||
team.specialization === 'mixed'
|
||||
) {
|
||||
specialization = team.specialization;
|
||||
} else {
|
||||
specialization = undefined;
|
||||
}
|
||||
|
||||
return {
|
||||
id: team.id,
|
||||
name: team.name,
|
||||
memberCount: team.memberCount,
|
||||
rating: team.rating,
|
||||
totalWins: team.totalWins,
|
||||
totalRaces: team.totalRaces,
|
||||
performanceLevel: team.performanceLevel,
|
||||
isRecruiting: team.isRecruiting,
|
||||
createdAt: team.createdAt,
|
||||
description: team.description ?? '',
|
||||
specialization: specialization ?? 'mixed',
|
||||
region: team.region ?? '',
|
||||
languages: team.languages ?? [],
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -1,6 +0,0 @@
|
||||
import { WalletDto, WalletTransactionDto } from '../dtos';
|
||||
import { WalletViewModel } from '../view-models';
|
||||
|
||||
export const presentWallet = (dto: WalletDto): WalletViewModel => {
|
||||
return new WalletViewModel(dto);
|
||||
};
|
||||
@@ -1,6 +0,0 @@
|
||||
import { WalletTransactionDto } from '../dtos';
|
||||
import { WalletTransactionViewModel } from '../view-models';
|
||||
|
||||
export const presentWalletTransaction = (dto: WalletTransactionDto): WalletTransactionViewModel => {
|
||||
return new WalletTransactionViewModel(dto);
|
||||
};
|
||||
Reference in New Issue
Block a user