wip league admin tools

This commit is contained in:
2025-12-28 12:04:12 +01:00
parent 5dc8c2399c
commit 6edf12fda8
401 changed files with 15365 additions and 6047 deletions

View File

@@ -27,8 +27,8 @@ export class GetLeagueMembershipsPresenter implements UseCaseOutputPort<GetLeagu
joinedAt: driver!.joinedAt.toDate().toISOString(),
...(driver!.bio ? { bio: driver!.bio.toString() } : {}),
},
role: membership.role.toString() as 'owner' | 'manager' | 'member',
joinedAt: membership.joinedAt.toDate(),
role: membership.role.toString() as 'owner' | 'admin' | 'steward' | 'member',
joinedAt: membership.joinedAt.toDate().toISOString(),
}));
this.result = {
memberships: {

View File

@@ -0,0 +1,64 @@
import type { UseCaseOutputPort } from '@core/shared/application';
import type { GetLeagueRosterMembersResult } from '@core/racing/application/use-cases/GetLeagueRosterMembersUseCase';
import type { GetLeagueRosterJoinRequestsResult } from '@core/racing/application/use-cases/GetLeagueRosterJoinRequestsUseCase';
import type { LeagueRosterMemberDTO } from '../dtos/LeagueRosterMemberDTO';
import type { LeagueRosterJoinRequestDTO } from '../dtos/LeagueRosterJoinRequestDTO';
import type { DriverDTO } from '../../driver/dtos/DriverDTO';
export class GetLeagueRosterMembersPresenter implements UseCaseOutputPort<GetLeagueRosterMembersResult> {
private viewModel: LeagueRosterMemberDTO[] | null = null;
reset(): void {
this.viewModel = null;
}
present(result: GetLeagueRosterMembersResult): void {
this.viewModel = result.members.map(({ membership, driver }) => ({
driverId: membership.driverId.toString(),
driver: this.mapDriver(driver),
role: membership.role.toString() as 'owner' | 'admin' | 'steward' | 'member',
joinedAt: membership.joinedAt.toDate().toISOString(),
}));
}
getViewModel(): LeagueRosterMemberDTO[] | null {
return this.viewModel;
}
private mapDriver(driver: GetLeagueRosterMembersResult['members'][number]['driver']): DriverDTO {
return {
id: driver.id,
iracingId: driver.iracingId.toString(),
name: driver.name.toString(),
country: driver.country.toString(),
joinedAt: driver.joinedAt.toDate().toISOString(),
...(driver.bio ? { bio: driver.bio.toString() } : {}),
};
}
}
export class GetLeagueRosterJoinRequestsPresenter implements UseCaseOutputPort<GetLeagueRosterJoinRequestsResult> {
private viewModel: LeagueRosterJoinRequestDTO[] | null = null;
reset(): void {
this.viewModel = null;
}
present(result: GetLeagueRosterJoinRequestsResult): void {
this.viewModel = result.joinRequests.map(req => ({
id: req.id,
leagueId: req.leagueId,
driverId: req.driverId,
requestedAt: req.requestedAt.toISOString(),
...(req.message ? { message: req.message } : {}),
driver: {
id: req.driver.id,
name: req.driver.name.toString(),
},
}));
}
getViewModel(): LeagueRosterJoinRequestDTO[] | null {
return this.viewModel;
}
}

View File

@@ -0,0 +1,41 @@
import { describe, expect, it } from 'vitest';
import { LeagueSchedulePresenter } from './LeagueSchedulePresenter';
import { Race } from '@core/racing/domain/entities/Race';
describe('LeagueSchedulePresenter', () => {
it('includes seasonId on the schedule DTO and serializes dates to ISO strings', () => {
const presenter = new LeagueSchedulePresenter();
const race = Race.create({
id: 'race-1',
leagueId: 'league-1',
scheduledAt: new Date('2025-01-02T20:00:00Z'),
track: 'Spa',
car: 'GT3',
});
presenter.present({
league: { id: 'league-1' },
seasonId: 'season-1',
published: false,
races: [{ race }],
} as any);
const vm = presenter.getViewModel() as any;
expect(vm).not.toBeNull();
expect(vm.seasonId).toBe('season-1');
expect(vm.published).toBe(false);
expect(Array.isArray(vm.races)).toBe(true);
expect(vm.races[0]).toMatchObject({
id: 'race-1',
name: 'Spa - GT3',
date: '2025-01-02T20:00:00.000Z',
});
// Guard: dates must be ISO strings (no Date objects)
expect(typeof vm.races[0].date).toBe('string');
expect(vm.races[0].date).toMatch(/^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}\.\d{3}Z$/);
});
});

View File

@@ -12,6 +12,8 @@ export class LeagueSchedulePresenter implements UseCaseOutputPort<GetLeagueSched
present(result: GetLeagueScheduleResult, leagueName?: string) {
this.result = {
seasonId: result.seasonId,
published: result.published,
races: result.races.map(race => ({
id: race.race.id,
name: `${race.race.track} - ${race.race.car}`,

View File

@@ -0,0 +1,105 @@
import type { UseCaseOutputPort } from '@core/shared/application';
import type {
CreateLeagueScheduleRaceOutputDTO,
LeagueScheduleRaceMutationSuccessDTO,
} from '../dtos/LeagueScheduleRaceAdminDTO';
import type { LeagueSeasonSchedulePublishOutputDTO } from '../dtos/LeagueSeasonSchedulePublishDTO';
import type { CreateLeagueSeasonScheduleRaceResult } from '@core/racing/application/use-cases/CreateLeagueSeasonScheduleRaceUseCase';
import type { UpdateLeagueSeasonScheduleRaceResult } from '@core/racing/application/use-cases/UpdateLeagueSeasonScheduleRaceUseCase';
import type { DeleteLeagueSeasonScheduleRaceResult } from '@core/racing/application/use-cases/DeleteLeagueSeasonScheduleRaceUseCase';
import type { PublishLeagueSeasonScheduleResult } from '@core/racing/application/use-cases/PublishLeagueSeasonScheduleUseCase';
import type { UnpublishLeagueSeasonScheduleResult } from '@core/racing/application/use-cases/UnpublishLeagueSeasonScheduleUseCase';
export class CreateLeagueSeasonScheduleRacePresenter
implements UseCaseOutputPort<CreateLeagueSeasonScheduleRaceResult>
{
private responseModel: CreateLeagueScheduleRaceOutputDTO | null = null;
present(result: CreateLeagueSeasonScheduleRaceResult): void {
this.responseModel = { raceId: result.raceId };
}
getResponseModel(): CreateLeagueScheduleRaceOutputDTO | null {
return this.responseModel;
}
reset(): void {
this.responseModel = null;
}
}
export class UpdateLeagueSeasonScheduleRacePresenter
implements UseCaseOutputPort<UpdateLeagueSeasonScheduleRaceResult>
{
private responseModel: LeagueScheduleRaceMutationSuccessDTO | null = null;
present(result: UpdateLeagueSeasonScheduleRaceResult): void {
void result;
this.responseModel = { success: true };
}
getResponseModel(): LeagueScheduleRaceMutationSuccessDTO | null {
return this.responseModel;
}
reset(): void {
this.responseModel = null;
}
}
export class DeleteLeagueSeasonScheduleRacePresenter
implements UseCaseOutputPort<DeleteLeagueSeasonScheduleRaceResult>
{
private responseModel: LeagueScheduleRaceMutationSuccessDTO | null = null;
present(result: DeleteLeagueSeasonScheduleRaceResult): void {
void result;
this.responseModel = { success: true };
}
getResponseModel(): LeagueScheduleRaceMutationSuccessDTO | null {
return this.responseModel;
}
reset(): void {
this.responseModel = null;
}
}
export class PublishLeagueSeasonSchedulePresenter
implements UseCaseOutputPort<PublishLeagueSeasonScheduleResult>
{
private responseModel: LeagueSeasonSchedulePublishOutputDTO | null = null;
present(result: PublishLeagueSeasonScheduleResult): void {
this.responseModel = { success: true, published: result.published };
}
getResponseModel(): LeagueSeasonSchedulePublishOutputDTO | null {
return this.responseModel;
}
reset(): void {
this.responseModel = null;
}
}
export class UnpublishLeagueSeasonSchedulePresenter
implements UseCaseOutputPort<UnpublishLeagueSeasonScheduleResult>
{
private responseModel: LeagueSeasonSchedulePublishOutputDTO | null = null;
present(result: UnpublishLeagueSeasonScheduleResult): void {
this.responseModel = { success: true, published: result.published };
}
getResponseModel(): LeagueSeasonSchedulePublishOutputDTO | null {
return this.responseModel;
}
reset(): void {
this.responseModel = null;
}
}

View File

@@ -10,11 +10,9 @@ export class RejectLeagueJoinRequestPresenter implements UseCaseOutputPort<Rejec
}
present(result: RejectLeagueJoinRequestResult): void {
void result;
this.result = {
success: true,
message: 'Join request rejected successfully',
success: result.success,
message: result.message,
};
}