resolve todos in website

This commit is contained in:
2025-12-20 12:22:48 +01:00
parent a87cf27fb9
commit 20588e1c0b
39 changed files with 1238 additions and 359 deletions

View File

@@ -85,4 +85,9 @@ export class RacesApiClient extends BaseApiClient {
complete(raceId: string): Promise<void> {
return this.post<void>(`/races/${raceId}/complete`, {});
}
/** Re-open race */
reopen(raceId: string): Promise<void> {
return this.post<void>(`/races/${raceId}/reopen`, {});
}
}

View File

@@ -14,7 +14,12 @@ describe('RaceService', () => {
getDetail: vi.fn(),
getPageData: vi.fn(),
getTotal: vi.fn(),
} as Mocked<RacesApiClient>;
register: vi.fn(),
withdraw: vi.fn(),
cancel: vi.fn(),
complete: vi.fn(),
reopen: vi.fn(),
} as unknown as Mocked<RacesApiClient>;
service = new RaceService(mockApiClient);
});
@@ -131,4 +136,22 @@ describe('RaceService', () => {
await expect(service.getRacesTotal()).rejects.toThrow('API call failed');
});
});
describe('reopenRace', () => {
it('should call apiClient.reopen with raceId', async () => {
const raceId = 'race-123';
await service.reopenRace(raceId);
expect(mockApiClient.reopen).toHaveBeenCalledWith(raceId);
});
it('should propagate errors from apiClient.reopen', async () => {
const raceId = 'race-123';
const error = new Error('API call failed');
mockApiClient.reopen.mockRejectedValue(error);
await expect(service.reopenRace(raceId)).rejects.toThrow('API call failed');
});
});
});

View File

@@ -78,6 +78,12 @@ export class RaceService {
await this.apiClient.complete(raceId);
}
/**
* Re-open a race
*/
async reopenRace(raceId: string): Promise<void> {
await this.apiClient.reopen(raceId);
}
/**
* Find races by league ID

View File

@@ -14,6 +14,9 @@ export class ProtestViewModel {
status: string;
reviewedAt?: string;
decisionNotes?: string;
incident?: { lap?: number } | null;
proofVideoUrl?: string | null;
comment?: string | null;
constructor(dto: ProtestDTO) {
this.id = dto.id;

View File

@@ -252,6 +252,36 @@ describe('RaceDetailViewModel', () => {
expect(viewModel.registrationStatusMessage).toBe('Registration not available');
});
it('should expose canReopenRace for completed and cancelled statuses', () => {
const completedVm = new RaceDetailViewModel({
race: createMockRace({ status: 'completed' }),
league: createMockLeague(),
entryList: [],
registration: createMockRegistration(),
userResult: null,
});
const cancelledVm = new RaceDetailViewModel({
race: createMockRace({ status: 'cancelled' as any }),
league: createMockLeague(),
entryList: [],
registration: createMockRegistration(),
userResult: null,
});
const upcomingVm = new RaceDetailViewModel({
race: createMockRace({ status: 'upcoming' }),
league: createMockLeague(),
entryList: [],
registration: createMockRegistration(),
userResult: null,
});
expect(completedVm.canReopenRace).toBe(true);
expect(cancelledVm.canReopenRace).toBe(true);
expect(upcomingVm.canReopenRace).toBe(false);
});
it('should handle error property', () => {
const viewModel = new RaceDetailViewModel({
race: createMockRace(),

View File

@@ -70,4 +70,10 @@ export class RaceDetailViewModel {
if (this.canRegister) return 'You can register for this race';
return 'Registration not available';
}
/** UI-specific: Whether race can be re-opened */
get canReopenRace(): boolean {
if (!this.race) return false;
return this.race.status === 'completed' || this.race.status === 'cancelled';
}
}