website refactor
This commit is contained in:
@@ -35,11 +35,11 @@ export class LeagueDetailViewDataBuilder {
|
||||
|
||||
// Calculate info data
|
||||
const membersCount = Array.isArray(memberships.members) ? memberships.members.length : 0;
|
||||
const completedRacesCount = races.filter(r => {
|
||||
const status = (r as any).status;
|
||||
return status === 'completed' || status === 'past';
|
||||
}).length;
|
||||
|
||||
|
||||
// League overview wants total races, not just completed.
|
||||
// (In seed/demo data many races are `status: running`, which should still count.)
|
||||
const racesCount = races.length;
|
||||
|
||||
// Compute real avgSOF from races
|
||||
const racesWithSOF = races.filter(r => {
|
||||
const sof = (r as any).strengthOfField;
|
||||
@@ -48,12 +48,25 @@ export class LeagueDetailViewDataBuilder {
|
||||
const avgSOF = racesWithSOF.length > 0
|
||||
? Math.round(racesWithSOF.reduce((sum, r) => sum + ((r as any).strengthOfField || 0), 0) / racesWithSOF.length)
|
||||
: null;
|
||||
|
||||
if (process.env.NODE_ENV !== 'production') {
|
||||
const race0 = races.length > 0 ? races[0] : null;
|
||||
console.info(
|
||||
'[LeagueDetailViewDataBuilder] leagueId=%s members=%d races=%d racesWithSOF=%d avgSOF=%s race0=%o',
|
||||
league.id,
|
||||
membersCount,
|
||||
racesCount,
|
||||
racesWithSOF.length,
|
||||
String(avgSOF),
|
||||
race0,
|
||||
);
|
||||
}
|
||||
|
||||
const info: LeagueInfoData = {
|
||||
name: league.name,
|
||||
description: league.description || '',
|
||||
membersCount,
|
||||
racesCount: completedRacesCount,
|
||||
racesCount,
|
||||
avgSOF,
|
||||
structure: `Solo • ${league.settings?.maxDrivers ?? 32} max`,
|
||||
scoring: scoringConfig?.scoringPresetId || 'Standard',
|
||||
|
||||
@@ -8,6 +8,8 @@ describe('DashboardService', () => {
|
||||
let service: DashboardService;
|
||||
|
||||
beforeEach(() => {
|
||||
process.env.API_BASE_URL = 'http://localhost:3001';
|
||||
|
||||
mockApiClient = {
|
||||
getDashboardOverview: vi.fn(),
|
||||
getAnalyticsMetrics: vi.fn(),
|
||||
|
||||
@@ -12,6 +12,8 @@ describe('LandingService', () => {
|
||||
let service: LandingService;
|
||||
|
||||
beforeEach(() => {
|
||||
process.env.API_BASE_URL = 'http://localhost:3001';
|
||||
|
||||
mockRacesApi = {
|
||||
getPageData: vi.fn(),
|
||||
} as unknown as Mocked<RacesApiClient>;
|
||||
|
||||
@@ -1,15 +1,18 @@
|
||||
import { describe, it, expect, vi, Mocked, beforeEach } from 'vitest';
|
||||
import { LeagueService } from './LeagueService';
|
||||
import { LeaguesApiClient } from '@/lib/api/leagues/LeaguesApiClient';
|
||||
import { RacesApiClient } from '@/lib/api/races/RacesApiClient';
|
||||
import type { CreateLeagueInputDTO } from '@/lib/types/generated/CreateLeagueInputDTO';
|
||||
import type { CreateLeagueOutputDTO } from '@/lib/types/generated/CreateLeagueOutputDTO';
|
||||
import type { RemoveLeagueMemberOutputDTO } from '@/lib/types/generated/RemoveLeagueMemberOutputDTO';
|
||||
|
||||
describe('LeagueService', () => {
|
||||
let mockApiClient: Mocked<LeaguesApiClient>;
|
||||
let mockRacesApiClient: Mocked<RacesApiClient>;
|
||||
let service: LeagueService;
|
||||
|
||||
beforeEach(() => {
|
||||
process.env.API_BASE_URL = process.env.API_BASE_URL || 'http://localhost:3001';
|
||||
mockApiClient = {
|
||||
getAllWithCapacity: vi.fn(),
|
||||
getAllWithCapacityAndScoring: vi.fn(),
|
||||
@@ -17,12 +20,20 @@ describe('LeagueService', () => {
|
||||
getTotal: vi.fn(),
|
||||
getSchedule: vi.fn(),
|
||||
getMemberships: vi.fn(),
|
||||
getLeagueConfig: vi.fn(),
|
||||
create: vi.fn(),
|
||||
removeRosterMember: vi.fn(),
|
||||
updateRosterMemberRole: vi.fn(),
|
||||
} as unknown as Mocked<LeaguesApiClient>;
|
||||
|
||||
mockRacesApiClient = {
|
||||
getPageData: vi.fn(),
|
||||
} as unknown as Mocked<RacesApiClient>;
|
||||
|
||||
mockApiClient.getLeagueConfig.mockResolvedValue({ form: null } as any);
|
||||
|
||||
service = new LeagueService(mockApiClient);
|
||||
(service as any).racesApiClient = mockRacesApiClient;
|
||||
});
|
||||
|
||||
describe('getAllLeagues', () => {
|
||||
@@ -145,6 +156,43 @@ describe('LeagueService', () => {
|
||||
});
|
||||
});
|
||||
|
||||
describe('getLeagueDetailData', () => {
|
||||
it('should use races page-data to enrich races with status/strengthOfField', async () => {
|
||||
const leagueId = 'league-123';
|
||||
const league = { id: leagueId, name: 'League One', createdAt: '2024-01-01T00:00:00Z' } as any;
|
||||
|
||||
mockApiClient.getAllWithCapacityAndScoring.mockResolvedValue({ totalCount: 1, leagues: [league] } as any);
|
||||
mockApiClient.getMemberships.mockResolvedValue({ members: [] } as any);
|
||||
mockRacesApiClient.getPageData.mockResolvedValue({
|
||||
races: [
|
||||
{
|
||||
id: 'race-1',
|
||||
track: 'Monza',
|
||||
car: 'GT3',
|
||||
scheduledAt: '2026-01-01T00:00:00.000Z',
|
||||
status: 'running',
|
||||
leagueId,
|
||||
leagueName: 'League One',
|
||||
strengthOfField: 2500,
|
||||
isUpcoming: false,
|
||||
isLive: true,
|
||||
isPast: false,
|
||||
},
|
||||
],
|
||||
} as any);
|
||||
|
||||
const result = await service.getLeagueDetailData(leagueId);
|
||||
|
||||
expect(result.isOk()).toBe(true);
|
||||
const dto = result.unwrap();
|
||||
expect(dto.races).toHaveLength(1);
|
||||
expect((dto.races[0] as any).status).toBe('running');
|
||||
expect((dto.races[0] as any).strengthOfField).toBe(2500);
|
||||
expect(dto.races[0]!.name).toBe('Monza - GT3');
|
||||
expect(dto.races[0]!.date).toBe('2026-01-01T00:00:00.000Z');
|
||||
});
|
||||
});
|
||||
|
||||
describe('getLeagueMemberships', () => {
|
||||
it('should call apiClient.getMemberships and return DTO', async () => {
|
||||
const leagueId = 'league-123';
|
||||
|
||||
@@ -58,6 +58,7 @@ export interface LeagueDetailData {
|
||||
*/
|
||||
@injectable()
|
||||
export class LeagueService implements Service {
|
||||
private readonly baseUrl: string;
|
||||
private apiClient: LeaguesApiClient;
|
||||
private driversApiClient: DriversApiClient;
|
||||
private sponsorsApiClient: SponsorsApiClient;
|
||||
@@ -65,6 +66,7 @@ export class LeagueService implements Service {
|
||||
|
||||
constructor(@unmanaged() apiClient?: LeaguesApiClient) {
|
||||
const baseUrl = getWebsiteApiBaseUrl();
|
||||
this.baseUrl = baseUrl;
|
||||
const logger = new ConsoleLogger();
|
||||
const errorReporter = new EnhancedErrorReporter(logger, {
|
||||
showUserNotifications: false,
|
||||
@@ -148,11 +150,27 @@ export class LeagueService implements Service {
|
||||
|
||||
async getLeagueDetailData(leagueId: string): Promise<Result<LeagueDetailData, DomainError>> {
|
||||
try {
|
||||
const [apiDto, memberships, racesResponse] = await Promise.all([
|
||||
const [apiDto, memberships, racesPageData] = await Promise.all([
|
||||
this.apiClient.getAllWithCapacityAndScoring(),
|
||||
this.apiClient.getMemberships(leagueId),
|
||||
this.apiClient.getRaces(leagueId),
|
||||
this.racesApiClient.getPageData(leagueId),
|
||||
]);
|
||||
|
||||
if (process.env.NODE_ENV !== 'production') {
|
||||
const membershipCount = Array.isArray(memberships?.members) ? memberships.members.length : 0;
|
||||
const racesCount = Array.isArray(racesPageData?.races) ? racesPageData.races.length : 0;
|
||||
const race0 = racesCount > 0 ? racesPageData.races[0] : null;
|
||||
|
||||
console.info(
|
||||
'[LeagueService.getLeagueDetailData] baseUrl=%s leagueId=%s memberships=%d races=%d race0=%o',
|
||||
this.baseUrl,
|
||||
leagueId,
|
||||
membershipCount,
|
||||
racesCount,
|
||||
race0,
|
||||
);
|
||||
|
||||
}
|
||||
|
||||
if (!apiDto || !apiDto.leagues) {
|
||||
return Result.err({ type: 'notFound', message: 'Leagues not found' });
|
||||
@@ -189,12 +207,21 @@ export class LeagueService implements Service {
|
||||
console.warn('Failed to fetch league scoring config', e);
|
||||
}
|
||||
|
||||
const races: RaceDTO[] = (racesPageData.races || []).map((r) => ({
|
||||
id: r.id,
|
||||
name: `${r.track} - ${r.car}`,
|
||||
date: r.scheduledAt,
|
||||
leagueName: r.leagueName,
|
||||
status: r.status,
|
||||
strengthOfField: r.strengthOfField,
|
||||
})) as unknown as RaceDTO[];
|
||||
|
||||
return Result.ok({
|
||||
league,
|
||||
owner,
|
||||
scoringConfig,
|
||||
memberships,
|
||||
races: racesResponse.races,
|
||||
races,
|
||||
sponsors: [], // Sponsors integration can be added here
|
||||
});
|
||||
} catch (error: unknown) {
|
||||
|
||||
@@ -11,6 +11,8 @@ describe('SponsorService', () => {
|
||||
let mockApiClientInstance: any;
|
||||
|
||||
beforeEach(() => {
|
||||
process.env.API_BASE_URL = 'http://localhost:3001';
|
||||
|
||||
vi.clearAllMocks();
|
||||
service = new SponsorService();
|
||||
// @ts-ignore - accessing private property for testing
|
||||
|
||||
Reference in New Issue
Block a user