283 lines
9.8 KiB
TypeScript
283 lines
9.8 KiB
TypeScript
import { LeaguesApiClient } from "@/lib/api/leagues/LeaguesApiClient";
|
|
import { DriversApiClient } from "@/lib/api/drivers/DriversApiClient";
|
|
import type { LeagueConfigFormModel } from "@/lib/types/LeagueConfigFormModel";
|
|
import type { LeagueScoringPresetDTO } from "@/lib/types/generated/LeagueScoringPresetDTO";
|
|
import { LeagueSettingsViewModel } from "@/lib/view-models/LeagueSettingsViewModel";
|
|
import { DriverSummaryViewModel } from "@/lib/view-models/DriverSummaryViewModel";
|
|
import type { LeagueScoringPresetViewModel } from "@/lib/view-models/LeagueScoringPresetViewModel";
|
|
import type { CustomPointsConfig } from "@/lib/view-models/ScoringConfigurationViewModel";
|
|
|
|
/**
|
|
* League Settings Service
|
|
*
|
|
* Orchestrates league settings operations by coordinating API calls and view model creation.
|
|
* All dependencies are injected via constructor.
|
|
*/
|
|
export class LeagueSettingsService {
|
|
constructor(
|
|
private readonly leaguesApiClient: LeaguesApiClient,
|
|
private readonly driversApiClient: DriversApiClient
|
|
) {}
|
|
|
|
/**
|
|
* Get league settings with view model transformation
|
|
*/
|
|
async getLeagueSettings(leagueId: string): Promise<LeagueSettingsViewModel | null> {
|
|
try {
|
|
// Get league basic info (includes ownerId in DTO)
|
|
const allLeagues = await this.leaguesApiClient.getAllWithCapacity();
|
|
const leagueDto = allLeagues.leagues.find(l => l.id === leagueId);
|
|
if (!leagueDto) return null;
|
|
|
|
const league = {
|
|
id: leagueDto.id,
|
|
name: leagueDto.name,
|
|
ownerId: leagueDto.ownerId,
|
|
createdAt: leagueDto.createdAt || new Date().toISOString(),
|
|
};
|
|
|
|
// Get config
|
|
const configDto = await this.leaguesApiClient.getLeagueConfig(leagueId);
|
|
const config: LeagueConfigFormModel = (configDto.form ?? undefined) as unknown as LeagueConfigFormModel;
|
|
|
|
// Get presets
|
|
const presetsDto = await this.leaguesApiClient.getScoringPresets();
|
|
const presets: LeagueScoringPresetDTO[] = presetsDto.presets;
|
|
|
|
// Get leaderboard once so we can hydrate rating / rank for owner + members
|
|
const leaderboardDto = await this.driversApiClient.getLeaderboard();
|
|
const leaderboardByDriverId = new Map(
|
|
leaderboardDto.drivers.map(driver => [driver.id, driver])
|
|
);
|
|
|
|
// Get owner
|
|
const ownerDriver = await this.driversApiClient.getDriver(league.ownerId);
|
|
let owner: DriverSummaryViewModel | null = null;
|
|
if (ownerDriver) {
|
|
const ownerStats = leaderboardByDriverId.get(ownerDriver.id);
|
|
owner = new DriverSummaryViewModel({
|
|
driver: ownerDriver,
|
|
rating: ownerStats?.rating ?? null,
|
|
rank: ownerStats?.rank ?? null,
|
|
});
|
|
}
|
|
|
|
// Get members
|
|
const membershipsDto = await this.leaguesApiClient.getMemberships(leagueId);
|
|
const members: DriverSummaryViewModel[] = [];
|
|
for (const member of membershipsDto.members) {
|
|
if (member.driverId !== league.ownerId && member.role !== 'owner') {
|
|
const driver = await this.driversApiClient.getDriver(member.driverId);
|
|
if (driver) {
|
|
const memberStats = leaderboardByDriverId.get(driver.id);
|
|
members.push(new DriverSummaryViewModel({
|
|
driver,
|
|
rating: memberStats?.rating ?? null,
|
|
rank: memberStats?.rank ?? null,
|
|
}));
|
|
}
|
|
}
|
|
}
|
|
|
|
return new LeagueSettingsViewModel({
|
|
league,
|
|
config,
|
|
presets,
|
|
owner,
|
|
members,
|
|
});
|
|
} catch (error) {
|
|
console.error('Failed to load league settings:', error);
|
|
return null;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Transfer league ownership
|
|
*/
|
|
async transferOwnership(leagueId: string, currentOwnerId: string, newOwnerId: string): Promise<boolean> {
|
|
try {
|
|
const result = await this.leaguesApiClient.transferOwnership(leagueId, currentOwnerId, newOwnerId);
|
|
return result.success;
|
|
} catch (error) {
|
|
console.error('Failed to transfer ownership:', error);
|
|
throw error;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Select a scoring preset
|
|
*/
|
|
selectScoringPreset(
|
|
currentForm: LeagueConfigFormModel,
|
|
presetId: string
|
|
): LeagueConfigFormModel {
|
|
return {
|
|
...currentForm,
|
|
scoring: {
|
|
...currentForm.scoring,
|
|
patternId: presetId,
|
|
customScoringEnabled: false,
|
|
},
|
|
};
|
|
}
|
|
|
|
/**
|
|
* Toggle custom scoring
|
|
*/
|
|
toggleCustomScoring(currentForm: LeagueConfigFormModel): LeagueConfigFormModel {
|
|
return {
|
|
...currentForm,
|
|
scoring: {
|
|
...currentForm.scoring,
|
|
customScoringEnabled: !currentForm.scoring.customScoringEnabled,
|
|
},
|
|
};
|
|
}
|
|
|
|
/**
|
|
* Update championship settings
|
|
*/
|
|
updateChampionship(
|
|
currentForm: LeagueConfigFormModel,
|
|
key: keyof LeagueConfigFormModel['championships'],
|
|
value: boolean
|
|
): LeagueConfigFormModel {
|
|
return {
|
|
...currentForm,
|
|
championships: {
|
|
...currentForm.championships,
|
|
[key]: value,
|
|
},
|
|
};
|
|
}
|
|
|
|
/**
|
|
* Get preset emoji based on name
|
|
*/
|
|
getPresetEmoji(preset: LeagueScoringPresetViewModel): string {
|
|
const name = preset.name.toLowerCase();
|
|
if (name.includes('sprint') || name.includes('double')) return '⚡';
|
|
if (name.includes('endurance') || name.includes('long')) return '🏆';
|
|
if (name.includes('club') || name.includes('casual')) return '🏅';
|
|
return '🏁';
|
|
}
|
|
|
|
/**
|
|
* Get preset description based on name
|
|
*/
|
|
getPresetDescription(preset: LeagueScoringPresetViewModel): string {
|
|
const name = preset.name.toLowerCase();
|
|
if (name.includes('sprint')) return 'Sprint + Feature race';
|
|
if (name.includes('endurance')) return 'Long-form endurance';
|
|
if (name.includes('club')) return 'Casual league format';
|
|
return preset.sessionSummary;
|
|
}
|
|
|
|
/**
|
|
* Get preset info content for flyout
|
|
*/
|
|
getPresetInfoContent(presetName: string): { title: string; description: string; details: string[] } {
|
|
const name = presetName.toLowerCase();
|
|
if (name.includes('sprint')) {
|
|
return {
|
|
title: 'Sprint + Feature Format',
|
|
description: 'A two-race weekend format with a shorter sprint race and a longer feature race.',
|
|
details: [
|
|
'Sprint race typically awards reduced points (e.g., 8-6-4-3-2-1)',
|
|
'Feature race awards full points (e.g., 25-18-15-12-10-8-6-4-2-1)',
|
|
'Grid for feature often based on sprint results',
|
|
'Great for competitive leagues with time for multiple races',
|
|
],
|
|
};
|
|
}
|
|
if (name.includes('endurance') || name.includes('long')) {
|
|
return {
|
|
title: 'Endurance Format',
|
|
description: 'Long-form racing focused on consistency and strategy over raw pace.',
|
|
details: [
|
|
'Single race per weekend, longer duration (60-90+ minutes)',
|
|
'Higher points for finishing (rewards reliability)',
|
|
'Often includes mandatory pit stops',
|
|
'Best for serious leagues with dedicated racers',
|
|
],
|
|
};
|
|
}
|
|
if (name.includes('club') || name.includes('casual')) {
|
|
return {
|
|
title: 'Club/Casual Format',
|
|
description: 'Relaxed format perfect for community leagues and casual racing.',
|
|
details: [
|
|
'Simple points structure, easy to understand',
|
|
'Typically single race per weekend',
|
|
'Lower stakes, focus on participation',
|
|
'Great for beginners or mixed-skill leagues',
|
|
],
|
|
};
|
|
}
|
|
return {
|
|
title: 'Standard Race Format',
|
|
description: 'Traditional single-race weekend with standard F1-style points.',
|
|
details: [
|
|
'Points: 25-18-15-12-10-8-6-4-2-1 for top 10',
|
|
'Bonus points for pole position and fastest lap',
|
|
'One race per weekend',
|
|
'The most common format used in sim racing',
|
|
],
|
|
};
|
|
}
|
|
|
|
/**
|
|
* Get championship info content for flyout
|
|
*/
|
|
getChampionshipInfoContent(key: string): { title: string; description: string; details: string[] } {
|
|
const info: Record<string, { title: string; description: string; details: string[] }> = {
|
|
enableDriverChampionship: {
|
|
title: 'Driver Championship',
|
|
description: 'Track individual driver performance across all races in the season.',
|
|
details: [
|
|
'Each driver accumulates points based on race finishes',
|
|
'The driver with most points at season end wins',
|
|
'Standard in all racing leagues',
|
|
'Shows overall driver skill and consistency',
|
|
],
|
|
},
|
|
enableTeamChampionship: {
|
|
title: 'Team Championship',
|
|
description: 'Combine points from all drivers within a team for team standings.',
|
|
details: [
|
|
'All drivers\' points count toward team total',
|
|
'Rewards having consistent performers across the roster',
|
|
'Creates team strategy opportunities',
|
|
'Only available in Teams mode leagues',
|
|
],
|
|
},
|
|
enableNationsChampionship: {
|
|
title: 'Nations Cup',
|
|
description: 'Group drivers by nationality for international competition.',
|
|
details: [
|
|
'Drivers represent their country automatically',
|
|
'Points pooled by nationality',
|
|
'Adds international rivalry element',
|
|
'Great for diverse, international leagues',
|
|
],
|
|
},
|
|
enableTrophyChampionship: {
|
|
title: 'Trophy Championship',
|
|
description: 'A special category championship for specific classes or groups.',
|
|
details: [
|
|
'Custom category you define (e.g., Am drivers, rookies)',
|
|
'Separate standings from main championship',
|
|
'Encourages participation from all skill levels',
|
|
'Can be used for gentleman drivers, newcomers, etc.',
|
|
],
|
|
},
|
|
};
|
|
|
|
return info[key] || {
|
|
title: 'Championship',
|
|
description: 'A championship standings category.',
|
|
details: ['Enable to track this type of championship.'],
|
|
};
|
|
}
|
|
} |