279 lines
11 KiB
TypeScript
279 lines
11 KiB
TypeScript
import { LeaguesApiClient } from "@/lib/api/leagues/LeaguesApiClient";
|
|
import { DriversApiClient } from "@/lib/api/drivers/DriversApiClient";
|
|
import { SponsorsApiClient } from "@/lib/api/sponsors/SponsorsApiClient";
|
|
import { RacesApiClient } from "@/lib/api/races/RacesApiClient";
|
|
import { CreateLeagueInputDTO } from "@/lib/types/generated/CreateLeagueInputDTO";
|
|
import { LeagueWithCapacityDTO } from "@/lib/types/generated/LeagueWithCapacityDTO";
|
|
import { CreateLeagueViewModel } from "@/lib/view-models/CreateLeagueViewModel";
|
|
import { LeagueMembershipsViewModel } from "@/lib/view-models/LeagueMembershipsViewModel";
|
|
import { LeagueScheduleViewModel } from "@/lib/view-models/LeagueScheduleViewModel";
|
|
import { LeagueStandingsViewModel } from "@/lib/view-models/LeagueStandingsViewModel";
|
|
import { LeagueStatsViewModel } from "@/lib/view-models/LeagueStatsViewModel";
|
|
import { LeagueSummaryViewModel } from "@/lib/view-models/LeagueSummaryViewModel";
|
|
import { RemoveMemberViewModel } from "@/lib/view-models/RemoveMemberViewModel";
|
|
import { LeagueDetailViewModel } from "@/lib/view-models/LeagueDetailViewModel";
|
|
import { LeagueDetailPageViewModel, SponsorInfo } from "@/lib/view-models/LeagueDetailPageViewModel";
|
|
import { RaceViewModel } from "@/lib/view-models/RaceViewModel";
|
|
import { SubmitBlocker, ThrottleBlocker } from "@/lib/blockers";
|
|
import { RaceDTO } from "@/lib/types/generated/RaceDTO";
|
|
import { LeagueScoringConfigDTO } from "@/lib/types/LeagueScoringConfigDTO";
|
|
import { LeagueStatsDTO } from "@/lib/types/generated/LeagueStatsDTO";
|
|
import { LeagueMembershipsDTO } from "@/lib/types/generated/LeagueMembershipsDTO";
|
|
|
|
|
|
/**
|
|
* League Service
|
|
*
|
|
* Orchestrates league operations by coordinating API calls and view model creation.
|
|
* All dependencies are injected via constructor.
|
|
*/
|
|
export class LeagueService {
|
|
private readonly submitBlocker = new SubmitBlocker();
|
|
private readonly throttle = new ThrottleBlocker(500);
|
|
|
|
constructor(
|
|
private readonly apiClient: LeaguesApiClient,
|
|
private readonly driversApiClient: DriversApiClient,
|
|
private readonly sponsorsApiClient: SponsorsApiClient,
|
|
private readonly racesApiClient: RacesApiClient
|
|
) {}
|
|
|
|
/**
|
|
* Get all leagues with view model transformation
|
|
*/
|
|
async getAllLeagues(): Promise<LeagueSummaryViewModel[]> {
|
|
const dto = await this.apiClient.getAllWithCapacity();
|
|
return dto.leagues.map((league: LeagueWithCapacityDTO) => new LeagueSummaryViewModel(league));
|
|
}
|
|
|
|
/**
|
|
* Get league standings with view model transformation
|
|
*/
|
|
async getLeagueStandings(leagueId: string, currentUserId: string): Promise<LeagueStandingsViewModel> {
|
|
// Core standings (positions, points, driverIds)
|
|
const dto = await this.apiClient.getStandings(leagueId);
|
|
|
|
// League memberships (roles, statuses)
|
|
const membershipsDto = await this.apiClient.getMemberships(leagueId);
|
|
|
|
// Resolve unique drivers that appear in standings
|
|
const driverIds = Array.from(new Set(dto.standings.map(entry => entry.driverId)));
|
|
const driverDtos = await Promise.all(driverIds.map(id => this.driversApiClient.getDriver(id)));
|
|
const drivers = driverDtos.filter((d): d is NonNullable<typeof d> => d !== null);
|
|
|
|
const dtoWithExtras = {
|
|
standings: dto.standings,
|
|
drivers,
|
|
memberships: membershipsDto.members,
|
|
};
|
|
|
|
return new LeagueStandingsViewModel(dtoWithExtras, currentUserId);
|
|
}
|
|
|
|
/**
|
|
* Get league statistics
|
|
*/
|
|
async getLeagueStats(): Promise<LeagueStatsViewModel> {
|
|
const dto = await this.apiClient.getTotal();
|
|
return new LeagueStatsViewModel(dto);
|
|
}
|
|
|
|
/**
|
|
* Get league schedule
|
|
*/
|
|
async getLeagueSchedule(leagueId: string): Promise<LeagueScheduleViewModel> {
|
|
const dto = await this.apiClient.getSchedule(leagueId);
|
|
return new LeagueScheduleViewModel(dto);
|
|
}
|
|
|
|
/**
|
|
* Get league memberships
|
|
*/
|
|
async getLeagueMemberships(leagueId: string, currentUserId: string): Promise<LeagueMembershipsViewModel> {
|
|
const dto = await this.apiClient.getMemberships(leagueId);
|
|
return new LeagueMembershipsViewModel(dto, currentUserId);
|
|
}
|
|
|
|
/**
|
|
* Create a new league
|
|
*/
|
|
async createLeague(input: CreateLeagueInputDTO): Promise<void> {
|
|
if (!this.submitBlocker.canExecute() || !this.throttle.canExecute()) return;
|
|
|
|
this.submitBlocker.block();
|
|
this.throttle.block();
|
|
try {
|
|
await this.apiClient.create(input);
|
|
} finally {
|
|
this.submitBlocker.release();
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Remove a member from league
|
|
*/
|
|
async removeMember(leagueId: string, performerDriverId: string, targetDriverId: string): Promise<RemoveMemberViewModel> {
|
|
const dto = await this.apiClient.removeMember(leagueId, performerDriverId, targetDriverId);
|
|
return new RemoveMemberViewModel(dto);
|
|
}
|
|
|
|
/**
|
|
* Update a member's role in league
|
|
*/
|
|
async updateMemberRole(leagueId: string, performerDriverId: string, targetDriverId: string, newRole: string): Promise<{ success: boolean }> {
|
|
return this.apiClient.updateMemberRole(leagueId, performerDriverId, targetDriverId, newRole);
|
|
}
|
|
|
|
/**
|
|
* Get league detail with owner, membership, and sponsor info
|
|
*/
|
|
async getLeagueDetail(leagueId: string, currentDriverId: string): Promise<LeagueDetailViewModel | null> {
|
|
// For now, assume league data comes from getAllWithCapacity or a new endpoint
|
|
// Since API may not have detailed league, we'll mock or assume
|
|
// In real implementation, add getLeagueDetail to API
|
|
const allLeagues = await this.apiClient.getAllWithCapacity();
|
|
const leagueDto = allLeagues.leagues.find(l => l.id === leagueId);
|
|
if (!leagueDto) return null;
|
|
|
|
// LeagueWithCapacityDTO already carries core fields; fall back to placeholder description/owner when not provided
|
|
const league = {
|
|
id: leagueDto.id,
|
|
name: leagueDto.name,
|
|
description: (leagueDto as any).description ?? 'Description not available',
|
|
ownerId: (leagueDto as any).ownerId ?? 'owner-id',
|
|
};
|
|
|
|
// Get owner
|
|
const owner = await this.driversApiClient.getDriver(league.ownerId);
|
|
const ownerName = owner ? owner.name : `${league.ownerId.slice(0, 8)}...`;
|
|
|
|
// Get membership
|
|
const membershipsDto = await this.apiClient.getMemberships(leagueId);
|
|
const membership = membershipsDto.members.find(m => m.driverId === currentDriverId);
|
|
const isAdmin = membership ? ['admin', 'owner'].includes(membership.role) : false;
|
|
|
|
// Get main sponsor
|
|
let mainSponsor = null;
|
|
try {
|
|
const seasonsDto = await this.apiClient.getSeasons(leagueId);
|
|
const activeSeason = seasonsDto.seasons.find((s: any) => s.status === 'active') ?? seasonsDto.seasons[0];
|
|
if (activeSeason) {
|
|
const sponsorshipsDto = await this.apiClient.getSeasonSponsorships(activeSeason.id);
|
|
const mainSponsorship = sponsorshipsDto.sponsorships.find((s: any) => s.tier === 'main' && s.status === 'active');
|
|
if (mainSponsorship) {
|
|
const sponsor = await this.sponsorsApiClient.getSponsor(mainSponsorship.sponsorId);
|
|
if (sponsor) {
|
|
mainSponsor = {
|
|
name: sponsor.name,
|
|
logoUrl: sponsor.logoUrl ?? '',
|
|
websiteUrl: sponsor.websiteUrl ?? '',
|
|
};
|
|
}
|
|
}
|
|
}
|
|
} catch (error) {
|
|
console.warn('Failed to load main sponsor:', error);
|
|
}
|
|
|
|
return new LeagueDetailViewModel(
|
|
league.id,
|
|
league.name,
|
|
league.description,
|
|
league.ownerId,
|
|
ownerName,
|
|
mainSponsor,
|
|
isAdmin
|
|
);
|
|
}
|
|
|
|
/**
|
|
* Get comprehensive league detail page data
|
|
*/
|
|
async getLeagueDetailPageData(leagueId: string): Promise<LeagueDetailPageViewModel | null> {
|
|
try {
|
|
// Get league basic info
|
|
const allLeagues = await this.apiClient.getAllWithCapacity();
|
|
const league = allLeagues.leagues.find(l => l.id === leagueId);
|
|
if (!league) return null;
|
|
|
|
// Get owner
|
|
const owner = await this.driversApiClient.getDriver(league.ownerId);
|
|
|
|
// League scoring configuration is not exposed separately yet; use null to indicate "not configured" in the UI
|
|
const scoringConfig: LeagueScoringConfigDTO | null = null;
|
|
|
|
// Drivers list is limited to those present in memberships until a dedicated league-drivers endpoint exists
|
|
const memberships = await this.apiClient.getMemberships(leagueId);
|
|
const driverIds = memberships.members.map(m => m.driverId);
|
|
const driverDtos = await Promise.all(driverIds.map(id => this.driversApiClient.getDriver(id)));
|
|
const drivers = driverDtos.filter((d): d is NonNullable<typeof d> => d !== null);
|
|
|
|
// Get all races for this league via the leagues API helper
|
|
const leagueRaces = await this.apiClient.getRaces(leagueId);
|
|
const allRaces = leagueRaces.races.map(r => new RaceViewModel(r as RaceDTO));
|
|
|
|
// League stats endpoint currently returns global league statistics rather than per-league values
|
|
const leagueStats = await this.apiClient.getTotal();
|
|
|
|
// Get sponsors
|
|
const sponsors = await this.getLeagueSponsors(leagueId);
|
|
|
|
return new LeagueDetailPageViewModel(
|
|
league,
|
|
owner,
|
|
scoringConfig,
|
|
drivers,
|
|
memberships,
|
|
allRaces,
|
|
leagueStats,
|
|
sponsors
|
|
);
|
|
} catch (error) {
|
|
console.error('Failed to load league detail page data:', error);
|
|
return null;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Get sponsors for a league
|
|
*/
|
|
private async getLeagueSponsors(leagueId: string): Promise<SponsorInfo[]> {
|
|
try {
|
|
const seasons = await this.apiClient.getSeasons(leagueId);
|
|
const activeSeason = seasons.seasons.find((s: any) => s.status === 'active') ?? seasons.seasons[0];
|
|
|
|
if (!activeSeason) return [];
|
|
|
|
const sponsorships = await this.apiClient.getSeasonSponsorships(activeSeason.id);
|
|
const activeSponsorships = sponsorships.sponsorships.filter((s: any) => s.status === 'active');
|
|
|
|
const sponsorInfos: SponsorInfo[] = [];
|
|
for (const sponsorship of activeSponsorships) {
|
|
const sponsor = await this.sponsorsApiClient.getSponsor(sponsorship.sponsorId);
|
|
if (sponsor) {
|
|
// Tagline is not supplied by the sponsor API in this build; callers may derive one from marketing content if needed
|
|
sponsorInfos.push({
|
|
id: sponsor.id,
|
|
name: sponsor.name,
|
|
logoUrl: sponsor.logoUrl ?? '',
|
|
websiteUrl: sponsor.websiteUrl ?? '',
|
|
tier: sponsorship.tier,
|
|
tagline: '',
|
|
});
|
|
}
|
|
}
|
|
|
|
// Sort: main sponsors first, then secondary
|
|
sponsorInfos.sort((a, b) => {
|
|
if (a.tier === 'main' && b.tier !== 'main') return -1;
|
|
if (a.tier !== 'main' && b.tier === 'main') return 1;
|
|
return 0;
|
|
});
|
|
|
|
return sponsorInfos;
|
|
} catch (error) {
|
|
console.warn('Failed to load sponsors:', error);
|
|
return [];
|
|
}
|
|
}
|
|
} |