435 lines
17 KiB
TypeScript
435 lines
17 KiB
TypeScript
import { injectable, unmanaged } from 'inversify';
|
|
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 { CreateLeagueOutputDTO } from "@/lib/types/generated/CreateLeagueOutputDTO";
|
|
import type { MembershipRole } from "@/lib/types/MembershipRole";
|
|
import type { LeagueRosterJoinRequestDTO } from "@/lib/types/generated/LeagueRosterJoinRequestDTO";
|
|
import type { TotalLeaguesDTO } from '@/lib/types/generated/TotalLeaguesDTO';
|
|
import type { LeagueSeasonSummaryDTO } from '@/lib/types/generated/LeagueSeasonSummaryDTO';
|
|
import type { LeagueScheduleDTO } from '@/lib/types/generated/LeagueScheduleDTO';
|
|
import type { CreateLeagueScheduleRaceInputDTO } from '@/lib/types/generated/CreateLeagueScheduleRaceInputDTO';
|
|
import type { CreateLeagueScheduleRaceOutputDTO } from '@/lib/types/generated/CreateLeagueScheduleRaceOutputDTO';
|
|
import type { UpdateLeagueScheduleRaceInputDTO } from '@/lib/types/generated/UpdateLeagueScheduleRaceInputDTO';
|
|
import type { LeagueScheduleRaceMutationSuccessDTO } from '@/lib/types/generated/LeagueScheduleRaceMutationSuccessDTO';
|
|
import type { LeagueSeasonSchedulePublishOutputDTO } from '@/lib/types/generated/LeagueSeasonSchedulePublishOutputDTO';
|
|
import type { LeagueRosterMemberDTO } from '@/lib/types/generated/LeagueRosterMemberDTO';
|
|
import type { LeagueMembershipsDTO } from '@/lib/types/generated/LeagueMembershipsDTO';
|
|
import type { GetTeamDetailsOutputDTO } from '@/lib/types/generated/GetTeamDetailsOutputDTO';
|
|
import type { GetDriverOutputDTO } from '@/lib/types/generated/GetDriverOutputDTO';
|
|
import type { RaceDTO } from '@/lib/types/generated/RaceDTO';
|
|
import type { LeagueScoringConfigDTO } from '@/lib/types/generated/LeagueScoringConfigDTO';
|
|
import { TeamMemberViewModel } from '@/lib/view-models/TeamMemberViewModel';
|
|
import { TeamSummaryViewModel } from '@/lib/view-models/TeamSummaryViewModel';
|
|
import { TeamDetailsViewModel } from '@/lib/view-models/TeamDetailsViewModel';
|
|
import { Result } from '@/lib/contracts/Result';
|
|
import { DomainError, Service } from '@/lib/contracts/services/Service';
|
|
import { ConsoleLogger } from '@/lib/infrastructure/logging/ConsoleLogger';
|
|
import { EnhancedErrorReporter } from '@/lib/infrastructure/EnhancedErrorReporter';
|
|
import { getWebsiteApiBaseUrl } from '@/lib/config/apiBaseUrl';
|
|
import { isProductionEnvironment } from '@/lib/config/env';
|
|
import { AllLeaguesWithCapacityAndScoringDTO } from '@/lib/types/AllLeaguesWithCapacityAndScoringDTO';
|
|
import type { LeagueWithCapacityAndScoringDTO } from '@/lib/types/generated/LeagueWithCapacityAndScoringDTO';
|
|
|
|
export interface LeagueScheduleAdminData {
|
|
leagueId: string;
|
|
seasonId: string;
|
|
seasons: LeagueSeasonSummaryDTO[];
|
|
schedule: LeagueScheduleDTO;
|
|
}
|
|
|
|
export interface LeagueRosterAdminData {
|
|
leagueId: string;
|
|
members: LeagueRosterMemberDTO[];
|
|
joinRequests: LeagueRosterJoinRequestDTO[];
|
|
}
|
|
|
|
export interface LeagueDetailData {
|
|
league: LeagueWithCapacityAndScoringDTO;
|
|
owner: GetDriverOutputDTO | null;
|
|
scoringConfig: LeagueScoringConfigDTO | null;
|
|
memberships: LeagueMembershipsDTO;
|
|
races: RaceDTO[];
|
|
sponsors: any[];
|
|
}
|
|
|
|
/**
|
|
* League Service - DTO Only
|
|
*
|
|
* Returns Result<ApiDto, DomainError>. No ViewModels or UX logic.
|
|
* All client-side presentation logic must be handled by hooks/components.
|
|
* @server-safe
|
|
*/
|
|
@injectable()
|
|
export class LeagueService implements Service {
|
|
private apiClient: LeaguesApiClient;
|
|
private driversApiClient: DriversApiClient;
|
|
private sponsorsApiClient: SponsorsApiClient;
|
|
private racesApiClient: RacesApiClient;
|
|
|
|
constructor(@unmanaged() apiClient?: LeaguesApiClient) {
|
|
const baseUrl = getWebsiteApiBaseUrl();
|
|
const logger = new ConsoleLogger();
|
|
const errorReporter = new EnhancedErrorReporter(logger, {
|
|
showUserNotifications: false,
|
|
logToConsole: true,
|
|
reportToExternal: isProductionEnvironment(),
|
|
});
|
|
|
|
if (apiClient) {
|
|
this.apiClient = apiClient;
|
|
} else {
|
|
this.apiClient = new LeaguesApiClient(baseUrl, errorReporter, logger);
|
|
}
|
|
|
|
this.driversApiClient = new DriversApiClient(baseUrl, errorReporter, logger);
|
|
this.sponsorsApiClient = new SponsorsApiClient(baseUrl, errorReporter, logger);
|
|
this.racesApiClient = new RacesApiClient(baseUrl, errorReporter, logger);
|
|
}
|
|
|
|
async getLeagueStandings(leagueId: string): Promise<any> {
|
|
try {
|
|
const data = await this.apiClient.getStandings(leagueId);
|
|
return (data as any).value || data;
|
|
} catch (error: unknown) {
|
|
throw error;
|
|
}
|
|
}
|
|
|
|
async getLeagueStats(): Promise<any> {
|
|
try {
|
|
const data = await this.apiClient.getTotal();
|
|
return (data as any).value || data;
|
|
} catch (error: unknown) {
|
|
throw error;
|
|
}
|
|
}
|
|
|
|
async getLeagueSchedule(leagueId: string): Promise<any> {
|
|
try {
|
|
const data = await this.apiClient.getSchedule(leagueId);
|
|
return (data as any).value || data;
|
|
} catch (error: unknown) {
|
|
throw error;
|
|
}
|
|
}
|
|
|
|
async getLeagueMemberships(leagueId: string): Promise<any> {
|
|
try {
|
|
const data = await this.apiClient.getMemberships(leagueId);
|
|
return (data as any).value || data;
|
|
} catch (error: unknown) {
|
|
throw error;
|
|
}
|
|
}
|
|
|
|
async createLeague(input: CreateLeagueInputDTO): Promise<any> {
|
|
try {
|
|
const data = await this.apiClient.create(input);
|
|
return (data as any).value || data;
|
|
} catch (error: unknown) {
|
|
throw error;
|
|
}
|
|
}
|
|
|
|
async removeMember(leagueId: string, targetDriverId: string): Promise<any> {
|
|
try {
|
|
const dto = await this.apiClient.removeRosterMember(leagueId, targetDriverId);
|
|
return { success: dto.success };
|
|
} catch (error: unknown) {
|
|
throw error;
|
|
}
|
|
}
|
|
|
|
async getAllLeagues(): Promise<Result<AllLeaguesWithCapacityAndScoringDTO, DomainError>> {
|
|
try {
|
|
const dto = await this.apiClient.getAllWithCapacityAndScoring();
|
|
return Result.ok(dto);
|
|
} catch (error: unknown) {
|
|
return Result.err({ type: 'serverError', message: (error as Error).message || 'Failed to fetch leagues' });
|
|
}
|
|
}
|
|
|
|
async getLeagueDetailData(leagueId: string): Promise<Result<LeagueDetailData, DomainError>> {
|
|
try {
|
|
const [apiDto, memberships, racesResponse] = await Promise.all([
|
|
this.apiClient.getAllWithCapacityAndScoring(),
|
|
this.apiClient.getMemberships(leagueId),
|
|
this.apiClient.getRaces(leagueId),
|
|
]);
|
|
|
|
if (!apiDto || !apiDto.leagues) {
|
|
return Result.err({ type: 'notFound', message: 'Leagues not found' });
|
|
}
|
|
|
|
const league = apiDto.leagues.find(l => l.id === leagueId);
|
|
if (!league) {
|
|
return Result.err({ type: 'notFound', message: 'League not found' });
|
|
}
|
|
|
|
// Fetch owner if ownerId exists
|
|
let owner: GetDriverOutputDTO | null = null;
|
|
if (league.ownerId) {
|
|
owner = await this.driversApiClient.getDriver(league.ownerId);
|
|
}
|
|
|
|
// Fetch scoring config if available
|
|
let scoringConfig: LeagueScoringConfigDTO | null = null;
|
|
try {
|
|
const config = await this.apiClient.getLeagueConfig(leagueId);
|
|
if (config.form?.scoring) {
|
|
// Map form scoring to LeagueScoringConfigDTO if possible, or just use partial
|
|
scoringConfig = {
|
|
leagueId,
|
|
seasonId: '', // Not available in this context
|
|
gameId: '',
|
|
gameName: '',
|
|
scoringPresetId: (config.form.scoring as any).presetId,
|
|
dropPolicySummary: '',
|
|
championships: [],
|
|
};
|
|
}
|
|
} catch (e) {
|
|
console.warn('Failed to fetch league scoring config', e);
|
|
}
|
|
|
|
return Result.ok({
|
|
league,
|
|
owner,
|
|
scoringConfig,
|
|
memberships,
|
|
races: racesResponse.races,
|
|
sponsors: [], // Sponsors integration can be added here
|
|
});
|
|
} catch (error: unknown) {
|
|
console.error('LeagueService.getLeagueDetailData failed:', error);
|
|
return Result.err({ type: 'serverError', message: (error as Error).message || 'Failed to fetch league detail' });
|
|
}
|
|
}
|
|
|
|
async getScheduleAdminData(leagueId: string, seasonId?: string): Promise<Result<LeagueScheduleAdminData, DomainError>> {
|
|
try {
|
|
const seasons = await this.apiClient.getSeasons(leagueId);
|
|
|
|
if (!seasons || seasons.length === 0) {
|
|
return Result.err({ type: 'notFound', message: 'No seasons found for league' });
|
|
}
|
|
|
|
const targetSeasonId = seasonId || (seasons.find(s => s.status === 'active')?.seasonId || seasons[0].seasonId);
|
|
const schedule = await this.apiClient.getSchedule(leagueId, targetSeasonId);
|
|
|
|
return Result.ok({
|
|
leagueId,
|
|
seasonId: targetSeasonId,
|
|
seasons,
|
|
schedule,
|
|
});
|
|
} catch (error: unknown) {
|
|
console.error('LeagueService.getScheduleAdminData failed:', error);
|
|
return Result.err({ type: 'serverError', message: (error as Error).message || 'Failed to fetch schedule admin data' });
|
|
}
|
|
}
|
|
|
|
async getRosterAdminData(leagueId: string): Promise<Result<LeagueRosterAdminData, DomainError>> {
|
|
try {
|
|
const [members, joinRequests] = await Promise.all([
|
|
this.apiClient.getAdminRosterMembers(leagueId),
|
|
this.apiClient.getAdminRosterJoinRequests(leagueId),
|
|
]);
|
|
|
|
return Result.ok({
|
|
leagueId,
|
|
members,
|
|
joinRequests,
|
|
});
|
|
} catch (error: unknown) {
|
|
console.error('LeagueService.getRosterAdminData failed:', error);
|
|
return Result.err({ type: 'serverError', message: (error as Error).message || 'Failed to fetch roster data' });
|
|
}
|
|
}
|
|
|
|
async getLeagueSeasons(leagueId: string): Promise<Result<LeagueSeasonSummaryDTO[], DomainError>> {
|
|
try {
|
|
const data = await this.apiClient.getSeasons(leagueId);
|
|
return Result.ok(data);
|
|
} catch (error: unknown) {
|
|
return Result.err({ type: 'serverError', message: (error as Error).message || 'Failed to fetch league seasons' });
|
|
}
|
|
}
|
|
|
|
async getLeagueSeasonSummaries(leagueId: string): Promise<Result<LeagueSeasonSummaryDTO[], DomainError>> {
|
|
return this.getLeagueSeasons(leagueId);
|
|
}
|
|
|
|
async getAdminSchedule(leagueId: string, seasonId: string): Promise<Result<LeagueScheduleDTO, DomainError>> {
|
|
try {
|
|
const data = await this.apiClient.getSchedule(leagueId, seasonId);
|
|
return Result.ok(data);
|
|
} catch (error: unknown) {
|
|
return Result.err({ type: 'serverError', message: (error as Error).message || 'Failed to fetch admin schedule' });
|
|
}
|
|
}
|
|
|
|
async publishAdminSchedule(leagueId: string, seasonId: string): Promise<Result<LeagueSeasonSchedulePublishOutputDTO, DomainError>> {
|
|
try {
|
|
const data = await this.apiClient.publishSeasonSchedule(leagueId, seasonId);
|
|
return Result.ok(data);
|
|
} catch (error: unknown) {
|
|
return Result.err({ type: 'serverError', message: (error as Error).message || 'Failed to publish schedule' });
|
|
}
|
|
}
|
|
|
|
async unpublishAdminSchedule(leagueId: string, seasonId: string): Promise<Result<LeagueSeasonSchedulePublishOutputDTO, DomainError>> {
|
|
try {
|
|
const data = await this.apiClient.unpublishSeasonSchedule(leagueId, seasonId);
|
|
return Result.ok(data);
|
|
} catch (error: unknown) {
|
|
return Result.err({ type: 'serverError', message: (error as Error).message || 'Failed to unpublish schedule' });
|
|
}
|
|
}
|
|
|
|
async createAdminScheduleRace(
|
|
leagueId: string,
|
|
seasonId: string,
|
|
input: { track: string; car: string; scheduledAtIso: string },
|
|
): Promise<Result<CreateLeagueScheduleRaceOutputDTO, DomainError>> {
|
|
try {
|
|
const payload: CreateLeagueScheduleRaceInputDTO = { ...input, example: '' };
|
|
const data = await this.apiClient.createSeasonScheduleRace(leagueId, seasonId, payload);
|
|
return Result.ok(data);
|
|
} catch (error: unknown) {
|
|
return Result.err({ type: 'serverError', message: (error as Error).message || 'Failed to create race' });
|
|
}
|
|
}
|
|
|
|
async updateAdminScheduleRace(
|
|
leagueId: string,
|
|
seasonId: string,
|
|
raceId: string,
|
|
input: Partial<{ track: string; car: string; scheduledAtIso: string }>,
|
|
): Promise<Result<LeagueScheduleRaceMutationSuccessDTO, DomainError>> {
|
|
try {
|
|
const payload: UpdateLeagueScheduleRaceInputDTO = { ...input, example: '' };
|
|
const data = await this.apiClient.updateSeasonScheduleRace(leagueId, seasonId, raceId, payload);
|
|
return Result.ok(data);
|
|
} catch (error: unknown) {
|
|
return Result.err({ type: 'serverError', message: (error as Error).message || 'Failed to update race' });
|
|
}
|
|
}
|
|
|
|
async deleteAdminScheduleRace(leagueId: string, seasonId: string, raceId: string): Promise<Result<LeagueScheduleRaceMutationSuccessDTO, DomainError>> {
|
|
try {
|
|
const data = await this.apiClient.deleteSeasonScheduleRace(leagueId, seasonId, raceId);
|
|
return Result.ok(data);
|
|
} catch (error: unknown) {
|
|
return Result.err({ type: 'serverError', message: (error as Error).message || 'Failed to delete race' });
|
|
}
|
|
}
|
|
|
|
async getLeagueScheduleDto(leagueId: string, seasonId: string): Promise<Result<LeagueScheduleDTO, DomainError>> {
|
|
return this.getAdminSchedule(leagueId, seasonId);
|
|
}
|
|
|
|
async publishLeagueSeasonSchedule(leagueId: string, seasonId: string): Promise<Result<LeagueSeasonSchedulePublishOutputDTO, DomainError>> {
|
|
return this.publishAdminSchedule(leagueId, seasonId);
|
|
}
|
|
|
|
async unpublishLeagueSeasonSchedule(leagueId: string, seasonId: string): Promise<Result<LeagueSeasonSchedulePublishOutputDTO, DomainError>> {
|
|
return this.unpublishAdminSchedule(leagueId, seasonId);
|
|
}
|
|
|
|
async createLeagueSeasonScheduleRace(
|
|
leagueId: string,
|
|
seasonId: string,
|
|
input: CreateLeagueScheduleRaceInputDTO,
|
|
): Promise<Result<CreateLeagueScheduleRaceOutputDTO, DomainError>> {
|
|
try {
|
|
const data = await this.apiClient.createSeasonScheduleRace(leagueId, seasonId, input);
|
|
return Result.ok(data);
|
|
} catch (error: unknown) {
|
|
return Result.err({ type: 'serverError', message: (error as Error).message || 'Failed to create race' });
|
|
}
|
|
}
|
|
|
|
async updateLeagueSeasonScheduleRace(
|
|
leagueId: string,
|
|
seasonId: string,
|
|
raceId: string,
|
|
input: UpdateLeagueScheduleRaceInputDTO,
|
|
): Promise<Result<LeagueScheduleRaceMutationSuccessDTO, DomainError>> {
|
|
try {
|
|
const data = await this.apiClient.updateSeasonScheduleRace(leagueId, seasonId, raceId, input);
|
|
return Result.ok(data);
|
|
} catch (error: unknown) {
|
|
return Result.err({ type: 'serverError', message: (error as Error).message || 'Failed to update race' });
|
|
}
|
|
}
|
|
|
|
async deleteLeagueSeasonScheduleRace(
|
|
leagueId: string,
|
|
seasonId: string,
|
|
raceId: string,
|
|
): Promise<Result<LeagueScheduleRaceMutationSuccessDTO, DomainError>> {
|
|
return this.deleteAdminScheduleRace(leagueId, seasonId, raceId);
|
|
}
|
|
|
|
async updateMemberRole(leagueId: string, targetDriverId: string, newRole: MembershipRole): Promise<Result<{ success: boolean }, DomainError>> {
|
|
try {
|
|
const dto = await this.apiClient.updateRosterMemberRole(leagueId, targetDriverId, newRole);
|
|
return Result.ok({ success: dto.success });
|
|
} catch (error: unknown) {
|
|
return Result.err({ type: 'serverError', message: (error as Error).message || 'Failed to update member role' });
|
|
}
|
|
}
|
|
|
|
async getAdminRosterMembers(leagueId: string): Promise<Result<LeagueRosterMemberDTO[], DomainError>> {
|
|
try {
|
|
const data = await this.apiClient.getAdminRosterMembers(leagueId);
|
|
return Result.ok(data);
|
|
} catch (error: unknown) {
|
|
return Result.err({ type: 'serverError', message: (error as Error).message || 'Failed to fetch roster members' });
|
|
}
|
|
}
|
|
|
|
async getAdminRosterJoinRequests(leagueId: string): Promise<Result<LeagueRosterJoinRequestDTO[], DomainError>> {
|
|
try {
|
|
const data = await this.apiClient.getAdminRosterJoinRequests(leagueId);
|
|
return Result.ok(data);
|
|
} catch (error: unknown) {
|
|
return Result.err({ type: 'serverError', message: (error as Error).message || 'Failed to fetch join requests' });
|
|
}
|
|
}
|
|
|
|
async approveJoinRequest(leagueId: string, joinRequestId: string): Promise<Result<{ success: boolean }, DomainError>> {
|
|
try {
|
|
const dto = await this.apiClient.approveRosterJoinRequest(leagueId, joinRequestId);
|
|
return Result.ok({ success: dto.success });
|
|
} catch (error: unknown) {
|
|
return Result.err({ type: 'serverError', message: (error as Error).message || 'Failed to approve join request' });
|
|
}
|
|
}
|
|
|
|
async rejectJoinRequest(leagueId: string, joinRequestId: string): Promise<Result<{ success: boolean }, DomainError>> {
|
|
try {
|
|
const dto = await this.apiClient.rejectRosterJoinRequest(leagueId, joinRequestId);
|
|
return Result.ok({ success: dto.success });
|
|
} catch (error: unknown) {
|
|
return Result.err({ type: 'serverError', message: (error as Error).message || 'Failed to reject join request' });
|
|
}
|
|
}
|
|
|
|
async getLeagueDetail(): Promise<Result<never, DomainError>> {
|
|
return Result.err({ type: 'notImplemented', message: 'League detail endpoint not implemented' });
|
|
}
|
|
|
|
async getLeagueDetailPageData(): Promise<Result<never, DomainError>> {
|
|
return Result.err({ type: 'notImplemented', message: 'League detail page data endpoint not implemented' });
|
|
}
|
|
|
|
async getScoringPresets(): Promise<Result<never, DomainError>> {
|
|
return Result.err({ type: 'notImplemented', message: 'Scoring presets endpoint not implemented' });
|
|
}
|
|
}
|