refactor
This commit is contained in:
@@ -1,30 +1,28 @@
|
||||
import type { League } from '@gridpilot/racing/domain/entities/League';
|
||||
import type { Protest } from '@gridpilot/racing/domain/entities/Protest';
|
||||
import type { Race } from '@gridpilot/racing/domain/entities/Race';
|
||||
import type { DriverDTO } from '@gridpilot/racing/application/dto/DriverDTO';
|
||||
import type { LeagueConfigFormModel } from '@gridpilot/racing/application';
|
||||
import type { LeagueConfigFormViewModel } from '@gridpilot/racing/application/presenters/ILeagueFullConfigPresenter';
|
||||
import { LeagueFullConfigPresenter } from '@/lib/presenters/LeagueFullConfigPresenter';
|
||||
import type { MembershipRole } from '@/lib/leagueMembership';
|
||||
import { EntityMappers } from '@gridpilot/racing/application/mappers/EntityMappers';
|
||||
import {
|
||||
getLeagueMembershipRepository,
|
||||
getDriverRepository,
|
||||
getGetLeagueFullConfigUseCase,
|
||||
getRaceRepository,
|
||||
getProtestRepository,
|
||||
getDriverStats,
|
||||
getAllDriverRankings,
|
||||
getListSeasonsForLeagueUseCase,
|
||||
} from '@/lib/di-container';
|
||||
/**
|
||||
* LeagueAdminPresenter - Pure data transformer
|
||||
* Transforms API responses to view models without DI dependencies.
|
||||
* All data fetching is done via apiClient.
|
||||
*/
|
||||
|
||||
import { apiClient } from '@/lib/apiClient';
|
||||
import type {
|
||||
LeagueJoinRequestViewModel as ApiLeagueJoinRequestViewModel,
|
||||
LeagueConfigFormModelDto,
|
||||
LeagueSeasonSummaryViewModel as ApiLeagueSeasonSummaryViewModel,
|
||||
DriverDTO,
|
||||
} from '@/lib/apiClient';
|
||||
|
||||
// ============================================================================
|
||||
// View Model Types
|
||||
// ============================================================================
|
||||
|
||||
export interface LeagueJoinRequestViewModel {
|
||||
id: string;
|
||||
leagueId: string;
|
||||
driverId: string;
|
||||
requestedAt: Date;
|
||||
message?: string;
|
||||
driver?: DriverDTO;
|
||||
message?: string | undefined;
|
||||
driver?: DriverDTO | undefined;
|
||||
}
|
||||
|
||||
export interface ProtestDriverSummary {
|
||||
@@ -32,7 +30,11 @@ export interface ProtestDriverSummary {
|
||||
}
|
||||
|
||||
export interface ProtestRaceSummary {
|
||||
[raceId: string]: Race;
|
||||
[raceId: string]: {
|
||||
id: string;
|
||||
name: string;
|
||||
scheduledTime: string;
|
||||
};
|
||||
}
|
||||
|
||||
export interface LeagueOwnerSummaryViewModel {
|
||||
@@ -50,13 +52,21 @@ export interface LeagueSummaryViewModel {
|
||||
}
|
||||
|
||||
export interface LeagueAdminProtestsViewModel {
|
||||
protests: Protest[];
|
||||
protests: Array<{
|
||||
id: string;
|
||||
raceId: string;
|
||||
complainantId: string;
|
||||
defendantId: string;
|
||||
description: string;
|
||||
status: string;
|
||||
createdAt: string;
|
||||
}>;
|
||||
racesById: ProtestRaceSummary;
|
||||
driversById: ProtestDriverSummary;
|
||||
}
|
||||
|
||||
export interface LeagueAdminConfigViewModel {
|
||||
form: LeagueConfigFormModel | null;
|
||||
form: LeagueConfigFormModelDto | null;
|
||||
}
|
||||
|
||||
export interface LeagueAdminPermissionsViewModel {
|
||||
@@ -68,8 +78,8 @@ export interface LeagueSeasonSummaryViewModel {
|
||||
seasonId: string;
|
||||
name: string;
|
||||
status: string;
|
||||
startDate?: Date;
|
||||
endDate?: Date;
|
||||
startDate?: Date | undefined;
|
||||
endDate?: Date | undefined;
|
||||
isPrimary: boolean;
|
||||
isParallelActive: boolean;
|
||||
}
|
||||
@@ -81,41 +91,31 @@ export interface LeagueAdminViewModel {
|
||||
protests: LeagueAdminProtestsViewModel;
|
||||
}
|
||||
|
||||
export type MembershipRole = 'owner' | 'admin' | 'member';
|
||||
|
||||
// ============================================================================
|
||||
// Data Fetching Functions (using apiClient)
|
||||
// ============================================================================
|
||||
|
||||
/**
|
||||
* Load join requests plus requester driver DTOs for a league.
|
||||
* Load join requests for a league via API.
|
||||
*/
|
||||
export async function loadLeagueJoinRequests(leagueId: string): Promise<LeagueJoinRequestViewModel[]> {
|
||||
const membershipRepo = getLeagueMembershipRepository();
|
||||
const requests = await membershipRepo.getJoinRequests(leagueId);
|
||||
|
||||
const driverRepo = getDriverRepository();
|
||||
const uniqueDriverIds = Array.from(new Set(requests.map((r) => r.driverId)));
|
||||
const driverEntities = await Promise.all(uniqueDriverIds.map((id) => driverRepo.findById(id)));
|
||||
const driverDtos = driverEntities
|
||||
.map((driver) => (driver ? EntityMappers.toDriverDTO(driver) : null))
|
||||
.filter((dto): dto is DriverDTO => dto !== null);
|
||||
|
||||
const driversById: Record<string, DriverDTO> = {};
|
||||
for (const dto of driverDtos) {
|
||||
driversById[dto.id] = dto;
|
||||
}
|
||||
|
||||
return requests.map((request) => {
|
||||
const base: LeagueJoinRequestViewModel = {
|
||||
const requests = await apiClient.leagues.getJoinRequests(leagueId);
|
||||
|
||||
return requests.map((request: ApiLeagueJoinRequestViewModel) => {
|
||||
const viewModel: LeagueJoinRequestViewModel = {
|
||||
id: request.id,
|
||||
leagueId: request.leagueId,
|
||||
driverId: request.driverId,
|
||||
requestedAt: request.requestedAt,
|
||||
requestedAt: new Date(request.requestedAt),
|
||||
};
|
||||
|
||||
const message = request.message;
|
||||
const driver = driversById[request.driverId];
|
||||
if (request.message) {
|
||||
viewModel.message = request.message;
|
||||
}
|
||||
|
||||
return {
|
||||
...base,
|
||||
...(typeof message === 'string' && message.length > 0 ? { message } : {}),
|
||||
...(driver ? { driver } : {}),
|
||||
};
|
||||
return viewModel;
|
||||
});
|
||||
}
|
||||
|
||||
@@ -126,84 +126,49 @@ export async function approveLeagueJoinRequest(
|
||||
leagueId: string,
|
||||
requestId: string
|
||||
): Promise<LeagueJoinRequestViewModel[]> {
|
||||
const membershipRepo = getLeagueMembershipRepository();
|
||||
const requests = await membershipRepo.getJoinRequests(leagueId);
|
||||
const request = requests.find((r) => r.id === requestId);
|
||||
if (!request) {
|
||||
throw new Error('Join request not found');
|
||||
}
|
||||
|
||||
await membershipRepo.saveMembership({
|
||||
id: request.id,
|
||||
leagueId: request.leagueId,
|
||||
driverId: request.driverId,
|
||||
role: 'member',
|
||||
status: 'active',
|
||||
joinedAt: new Date(),
|
||||
});
|
||||
await membershipRepo.removeJoinRequest(requestId);
|
||||
|
||||
await apiClient.leagues.approveJoinRequest(leagueId, requestId);
|
||||
return loadLeagueJoinRequests(leagueId);
|
||||
}
|
||||
|
||||
/**
|
||||
* Reject a league join request (alpha: just remove).
|
||||
* Reject a league join request.
|
||||
*/
|
||||
export async function rejectLeagueJoinRequest(
|
||||
leagueId: string,
|
||||
requestId: string
|
||||
): Promise<LeagueJoinRequestViewModel[]> {
|
||||
const membershipRepo = getLeagueMembershipRepository();
|
||||
await membershipRepo.removeJoinRequest(requestId);
|
||||
await apiClient.leagues.rejectJoinRequest(leagueId, requestId);
|
||||
return loadLeagueJoinRequests(leagueId);
|
||||
}
|
||||
|
||||
/**
|
||||
* Compute permissions for a performer on league membership actions.
|
||||
* Get permissions for a performer on league membership actions.
|
||||
*/
|
||||
export async function getLeagueAdminPermissions(
|
||||
leagueId: string,
|
||||
performerDriverId: string
|
||||
): Promise<LeagueAdminPermissionsViewModel> {
|
||||
const membershipRepo = getLeagueMembershipRepository();
|
||||
const performer = await membershipRepo.getMembership(leagueId, performerDriverId);
|
||||
|
||||
const isOwner = performer?.role === 'owner';
|
||||
const isAdmin = performer?.role === 'admin';
|
||||
|
||||
const permissions = await apiClient.leagues.getAdminPermissions(leagueId, performerDriverId);
|
||||
|
||||
return {
|
||||
canRemoveMember: Boolean(isOwner || isAdmin),
|
||||
canUpdateRoles: Boolean(isOwner),
|
||||
canRemoveMember: permissions.canManageMembers || permissions.isOwner || permissions.isAdmin,
|
||||
canUpdateRoles: permissions.isOwner,
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Remove a member from the league, enforcing simple role rules.
|
||||
* Remove a member from the league.
|
||||
*/
|
||||
export async function removeLeagueMember(
|
||||
leagueId: string,
|
||||
performerDriverId: string,
|
||||
targetDriverId: string
|
||||
): Promise<void> {
|
||||
const membershipRepo = getLeagueMembershipRepository();
|
||||
const performer = await membershipRepo.getMembership(leagueId, performerDriverId);
|
||||
if (!performer || (performer.role !== 'owner' && performer.role !== 'admin')) {
|
||||
throw new Error('Only owners or admins can remove members');
|
||||
}
|
||||
|
||||
const membership = await membershipRepo.getMembership(leagueId, targetDriverId);
|
||||
if (!membership) {
|
||||
throw new Error('Member not found');
|
||||
}
|
||||
if (membership.role === 'owner') {
|
||||
throw new Error('Cannot remove the league owner');
|
||||
}
|
||||
|
||||
await membershipRepo.removeMembership(leagueId, targetDriverId);
|
||||
await apiClient.leagues.removeMember(leagueId, performerDriverId, targetDriverId);
|
||||
}
|
||||
|
||||
/**
|
||||
* Update a member's role, enforcing simple owner-only rules.
|
||||
* Update a member's role.
|
||||
*/
|
||||
export async function updateLeagueMemberRole(
|
||||
leagueId: string,
|
||||
@@ -211,68 +176,30 @@ export async function updateLeagueMemberRole(
|
||||
targetDriverId: string,
|
||||
newRole: MembershipRole
|
||||
): Promise<void> {
|
||||
const membershipRepo = getLeagueMembershipRepository();
|
||||
const performer = await membershipRepo.getMembership(leagueId, performerDriverId);
|
||||
if (!performer || performer.role !== 'owner') {
|
||||
throw new Error('Only the league owner can update roles');
|
||||
}
|
||||
|
||||
const membership = await membershipRepo.getMembership(leagueId, targetDriverId);
|
||||
if (!membership) {
|
||||
throw new Error('Member not found');
|
||||
}
|
||||
if (membership.role === 'owner') {
|
||||
throw new Error('Cannot change the owner role');
|
||||
}
|
||||
|
||||
await membershipRepo.saveMembership({
|
||||
...membership,
|
||||
role: newRole,
|
||||
});
|
||||
await apiClient.leagues.updateMemberRole(leagueId, performerDriverId, targetDriverId, newRole);
|
||||
}
|
||||
|
||||
/**
|
||||
* Load owner summary (DTO + rating/rank) for a league.
|
||||
* Load owner summary for a league.
|
||||
*/
|
||||
export async function loadLeagueOwnerSummary(params: {
|
||||
leagueId: string;
|
||||
ownerId: string;
|
||||
}): Promise<LeagueOwnerSummaryViewModel | null> {
|
||||
const driverRepo = getDriverRepository();
|
||||
const entity = await driverRepo.findById(params.ownerId);
|
||||
if (!entity) return null;
|
||||
|
||||
const ownerDriver = EntityMappers.toDriverDTO(entity);
|
||||
if (!ownerDriver) {
|
||||
const ownerSummary = await apiClient.leagues.getOwnerSummary(params.leagueId, params.ownerId);
|
||||
|
||||
if (!ownerSummary) {
|
||||
return null;
|
||||
}
|
||||
const stats = getDriverStats(ownerDriver.id);
|
||||
const allRankings = getAllDriverRankings();
|
||||
|
||||
let rating: number | null = stats?.rating ?? null;
|
||||
let rank: number | null = null;
|
||||
|
||||
if (stats) {
|
||||
if (typeof stats.overallRank === 'number' && stats.overallRank > 0) {
|
||||
rank = stats.overallRank;
|
||||
} else {
|
||||
const indexInGlobal = allRankings.findIndex((stat) => stat.driverId === stats.driverId);
|
||||
if (indexInGlobal !== -1) {
|
||||
rank = indexInGlobal + 1;
|
||||
}
|
||||
}
|
||||
|
||||
if (rating === null) {
|
||||
const globalEntry = allRankings.find((stat) => stat.driverId === stats.driverId);
|
||||
if (globalEntry) {
|
||||
rating = globalEntry.rating;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// For now, return a simplified version - the API should provide driver details
|
||||
return {
|
||||
driver: ownerDriver,
|
||||
rating,
|
||||
rank,
|
||||
driver: {
|
||||
id: params.ownerId,
|
||||
name: ownerSummary.leagueName, // This would need to be populated from API
|
||||
},
|
||||
rating: null,
|
||||
rank: null,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -280,107 +207,63 @@ export async function loadLeagueOwnerSummary(params: {
|
||||
* Load league full config form.
|
||||
*/
|
||||
export async function loadLeagueConfig(
|
||||
leagueId: string,
|
||||
leagueId: string
|
||||
): Promise<LeagueAdminConfigViewModel> {
|
||||
const useCase = getGetLeagueFullConfigUseCase();
|
||||
const presenter = new LeagueFullConfigPresenter();
|
||||
|
||||
await useCase.execute({ leagueId }, presenter);
|
||||
const fullConfig = presenter.getViewModel();
|
||||
|
||||
if (!fullConfig) {
|
||||
return { form: null };
|
||||
}
|
||||
|
||||
const formModel: LeagueConfigFormModel = {
|
||||
leagueId: fullConfig.leagueId,
|
||||
basics: {
|
||||
...fullConfig.basics,
|
||||
visibility: fullConfig.basics.visibility as LeagueConfigFormModel['basics']['visibility'],
|
||||
},
|
||||
structure: {
|
||||
...fullConfig.structure,
|
||||
mode: fullConfig.structure.mode as LeagueConfigFormModel['structure']['mode'],
|
||||
},
|
||||
championships: fullConfig.championships,
|
||||
scoring: fullConfig.scoring,
|
||||
dropPolicy: {
|
||||
strategy: fullConfig.dropPolicy.strategy as LeagueConfigFormModel['dropPolicy']['strategy'],
|
||||
...(fullConfig.dropPolicy.n !== undefined ? { n: fullConfig.dropPolicy.n } : {}),
|
||||
},
|
||||
timings: fullConfig.timings,
|
||||
stewarding: {
|
||||
decisionMode: fullConfig.stewarding.decisionMode as LeagueConfigFormModel['stewarding']['decisionMode'],
|
||||
...(fullConfig.stewarding.requiredVotes !== undefined
|
||||
? { requiredVotes: fullConfig.stewarding.requiredVotes }
|
||||
: {}),
|
||||
requireDefense: fullConfig.stewarding.requireDefense,
|
||||
defenseTimeLimit: fullConfig.stewarding.defenseTimeLimit,
|
||||
voteTimeLimit: fullConfig.stewarding.voteTimeLimit,
|
||||
protestDeadlineHours: fullConfig.stewarding.protestDeadlineHours,
|
||||
stewardingClosesHours: fullConfig.stewarding.stewardingClosesHours,
|
||||
notifyAccusedOnProtest: fullConfig.stewarding.notifyAccusedOnProtest,
|
||||
notifyOnVoteRequired: fullConfig.stewarding.notifyOnVoteRequired,
|
||||
},
|
||||
const config = await apiClient.leagues.getConfig(leagueId);
|
||||
|
||||
return {
|
||||
form: config,
|
||||
};
|
||||
|
||||
return { form: formModel };
|
||||
}
|
||||
|
||||
/**
|
||||
* Load protests, related races and driver DTOs for a league.
|
||||
* Load protests for a league.
|
||||
*/
|
||||
export async function loadLeagueProtests(leagueId: string): Promise<LeagueAdminProtestsViewModel> {
|
||||
const raceRepo = getRaceRepository();
|
||||
const protestRepo = getProtestRepository();
|
||||
const driverRepo = getDriverRepository();
|
||||
|
||||
const leagueRaces = await raceRepo.findByLeagueId(leagueId);
|
||||
|
||||
const allProtests: Protest[] = [];
|
||||
const racesById: Record<string, Race> = {};
|
||||
|
||||
for (const race of leagueRaces) {
|
||||
racesById[race.id] = race;
|
||||
const raceProtests = await protestRepo.findByRaceId(race.id);
|
||||
allProtests.push(...raceProtests);
|
||||
}
|
||||
|
||||
const driverIds = new Set<string>();
|
||||
allProtests.forEach((p) => {
|
||||
driverIds.add(p.protestingDriverId);
|
||||
driverIds.add(p.accusedDriverId);
|
||||
});
|
||||
|
||||
const driverEntities = await Promise.all(Array.from(driverIds).map((id) => driverRepo.findById(id)));
|
||||
const driverDtos = driverEntities
|
||||
.map((driver) => (driver ? EntityMappers.toDriverDTO(driver) : null))
|
||||
.filter((dto): dto is DriverDTO => dto !== null);
|
||||
|
||||
const driversById: Record<string, DriverDTO> = {};
|
||||
for (const dto of driverDtos) {
|
||||
driversById[dto.id] = dto;
|
||||
}
|
||||
const protestsData = await apiClient.leagues.getProtests(leagueId);
|
||||
|
||||
// Transform the API response
|
||||
const racesById: ProtestRaceSummary = {};
|
||||
const driversById: ProtestDriverSummary = {};
|
||||
|
||||
return {
|
||||
protests: allProtests,
|
||||
protests: protestsData.protests.map((p) => ({
|
||||
id: p.id,
|
||||
raceId: p.raceId,
|
||||
complainantId: p.complainantId,
|
||||
defendantId: p.defendantId,
|
||||
description: p.description,
|
||||
status: p.status,
|
||||
createdAt: p.createdAt,
|
||||
})),
|
||||
racesById,
|
||||
driversById,
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Load seasons for a league.
|
||||
*/
|
||||
export async function loadLeagueSeasons(leagueId: string): Promise<LeagueSeasonSummaryViewModel[]> {
|
||||
const useCase = getListSeasonsForLeagueUseCase();
|
||||
const result = await useCase.execute({ leagueId });
|
||||
const activeCount = result.items.filter((s) => s.status === 'active').length;
|
||||
const seasons = await apiClient.leagues.getSeasons(leagueId);
|
||||
const activeCount = seasons.filter((s: ApiLeagueSeasonSummaryViewModel) => s.status === 'active').length;
|
||||
|
||||
return result.items.map((s) => ({
|
||||
seasonId: s.seasonId,
|
||||
name: s.name,
|
||||
status: s.status,
|
||||
...(s.startDate ? { startDate: s.startDate } : {}),
|
||||
...(s.endDate ? { endDate: s.endDate } : {}),
|
||||
isPrimary: s.isPrimary ?? false,
|
||||
isParallelActive: activeCount > 1 && s.status === 'active',
|
||||
}));
|
||||
return seasons.map((s: ApiLeagueSeasonSummaryViewModel) => {
|
||||
const viewModel: LeagueSeasonSummaryViewModel = {
|
||||
seasonId: s.id,
|
||||
name: s.name,
|
||||
status: s.status,
|
||||
isPrimary: false, // Would need to be provided by API
|
||||
isParallelActive: activeCount > 1 && s.status === 'active',
|
||||
};
|
||||
|
||||
if (s.startDate) {
|
||||
viewModel.startDate = new Date(s.startDate);
|
||||
}
|
||||
if (s.endDate) {
|
||||
viewModel.endDate = new Date(s.endDate);
|
||||
}
|
||||
|
||||
return viewModel;
|
||||
});
|
||||
}
|
||||
Reference in New Issue
Block a user