This commit is contained in:
2025-12-05 12:24:38 +01:00
parent fb509607c1
commit 5a9cd28d5b
47 changed files with 5456 additions and 228 deletions

View File

@@ -0,0 +1,60 @@
export type LeagueStructureMode = 'solo' | 'fixedTeams';
export interface LeagueStructureFormDTO {
mode: LeagueStructureMode;
maxDrivers: number;
maxTeams?: number;
driversPerTeam?: number;
multiClassEnabled?: boolean;
}
export interface LeagueChampionshipsFormDTO {
enableDriverChampionship: boolean;
enableTeamChampionship: boolean;
enableNationsChampionship: boolean;
enableTrophyChampionship: boolean;
}
export interface LeagueScoringFormDTO {
patternId?: string; // e.g. 'sprint-main-driver', 'club-ladder-solo'
// For now, keep customScoring optional and simple:
customScoringEnabled?: boolean;
}
export interface LeagueDropPolicyFormDTO {
strategy: 'none' | 'bestNResults' | 'dropWorstN';
n?: number;
}
export interface LeagueTimingsFormDTO {
practiceMinutes?: number;
qualifyingMinutes: number;
sprintRaceMinutes?: number;
mainRaceMinutes: number;
sessionCount: number;
roundsPlanned?: number;
seasonStartDate?: string; // ISO date YYYY-MM-DD
raceStartTime?: string; // "HH:MM" 24h
timezoneId?: string; // IANA ID, e.g. "Europe/Berlin"
recurrenceStrategy?: 'weekly' | 'everyNWeeks' | 'monthlyNthWeekday';
intervalWeeks?: number;
weekdays?: import('../../domain/value-objects/Weekday').Weekday[];
monthlyOrdinal?: 1 | 2 | 3 | 4;
monthlyWeekday?: import('../../domain/value-objects/Weekday').Weekday;
}
export interface LeagueConfigFormModel {
leagueId?: string; // present for admin, omitted for create
basics: {
name: string;
description?: string;
visibility: 'public' | 'private';
gameId: string;
};
structure: LeagueStructureFormDTO;
championships: LeagueChampionshipsFormDTO;
scoring: LeagueScoringFormDTO;
dropPolicy: LeagueDropPolicyFormDTO;
timings: LeagueTimingsFormDTO;
}

View File

@@ -0,0 +1,114 @@
import type { LeagueTimingsFormDTO } from './LeagueConfigFormDTO';
import type { Weekday } from '../../domain/value-objects/Weekday';
import { RaceTimeOfDay } from '../../domain/value-objects/RaceTimeOfDay';
import { LeagueTimezone } from '../../domain/value-objects/LeagueTimezone';
import { WeekdaySet } from '../../domain/value-objects/WeekdaySet';
import { MonthlyRecurrencePattern } from '../../domain/value-objects/MonthlyRecurrencePattern';
import type { RecurrenceStrategy } from '../../domain/value-objects/RecurrenceStrategy';
import { RecurrenceStrategyFactory } from '../../domain/value-objects/RecurrenceStrategy';
import { SeasonSchedule } from '../../domain/value-objects/SeasonSchedule';
export interface LeagueScheduleDTO {
seasonStartDate: string;
raceStartTime: string;
timezoneId: string;
recurrenceStrategy: 'weekly' | 'everyNWeeks' | 'monthlyNthWeekday';
intervalWeeks?: number;
weekdays?: Weekday[];
monthlyOrdinal?: 1 | 2 | 3 | 4;
monthlyWeekday?: Weekday;
plannedRounds: number;
}
export interface LeagueSchedulePreviewDTO {
rounds: Array<{ roundNumber: number; scheduledAt: string; timezoneId: string }>;
summary: string;
}
export function leagueTimingsToScheduleDTO(
timings: LeagueTimingsFormDTO,
): LeagueScheduleDTO | null {
if (
!timings.seasonStartDate ||
!timings.raceStartTime ||
!timings.timezoneId ||
!timings.recurrenceStrategy ||
!timings.roundsPlanned
) {
return null;
}
return {
seasonStartDate: timings.seasonStartDate,
raceStartTime: timings.raceStartTime,
timezoneId: timings.timezoneId,
recurrenceStrategy: timings.recurrenceStrategy,
intervalWeeks: timings.intervalWeeks,
weekdays: timings.weekdays,
monthlyOrdinal: timings.monthlyOrdinal,
monthlyWeekday: timings.monthlyWeekday,
plannedRounds: timings.roundsPlanned,
};
}
export function scheduleDTOToSeasonSchedule(dto: LeagueScheduleDTO): SeasonSchedule {
if (!dto.seasonStartDate) {
throw new Error('seasonStartDate is required');
}
if (!dto.raceStartTime) {
throw new Error('raceStartTime is required');
}
if (!dto.timezoneId) {
throw new Error('timezoneId is required');
}
if (!dto.recurrenceStrategy) {
throw new Error('recurrenceStrategy is required');
}
if (!Number.isInteger(dto.plannedRounds) || dto.plannedRounds <= 0) {
throw new Error('plannedRounds must be a positive integer');
}
const startDate = new Date(dto.seasonStartDate);
if (Number.isNaN(startDate.getTime())) {
throw new Error(`seasonStartDate must be a valid date, got "${dto.seasonStartDate}"`);
}
const timeOfDay = RaceTimeOfDay.fromString(dto.raceStartTime);
const timezone = new LeagueTimezone(dto.timezoneId);
let recurrence: RecurrenceStrategy;
if (dto.recurrenceStrategy === 'weekly') {
if (!dto.weekdays || dto.weekdays.length === 0) {
throw new Error('weekdays are required for weekly recurrence');
}
recurrence = RecurrenceStrategyFactory.weekly(new WeekdaySet(dto.weekdays));
} else if (dto.recurrenceStrategy === 'everyNWeeks') {
if (!dto.weekdays || dto.weekdays.length === 0) {
throw new Error('weekdays are required for everyNWeeks recurrence');
}
if (dto.intervalWeeks == null) {
throw new Error('intervalWeeks is required for everyNWeeks recurrence');
}
recurrence = RecurrenceStrategyFactory.everyNWeeks(
dto.intervalWeeks,
new WeekdaySet(dto.weekdays),
);
} else if (dto.recurrenceStrategy === 'monthlyNthWeekday') {
if (!dto.monthlyOrdinal || !dto.monthlyWeekday) {
throw new Error('monthlyOrdinal and monthlyWeekday are required for monthlyNthWeekday');
}
const pattern = new MonthlyRecurrencePattern(dto.monthlyOrdinal, dto.monthlyWeekday);
recurrence = RecurrenceStrategyFactory.monthlyNthWeekday(pattern);
} else {
throw new Error(`Unknown recurrenceStrategy "${dto.recurrenceStrategy}"`);
}
return new SeasonSchedule({
startDate,
timeOfDay,
timezone,
recurrence,
plannedRounds: dto.plannedRounds,
});
}

View File

@@ -0,0 +1,20 @@
export interface LeagueScoringChampionshipDTO {
id: string;
name: string;
type: 'driver' | 'team' | 'nations' | 'trophy';
sessionTypes: string[];
pointsPreview: Array<{ sessionType: string; position: number; points: number }>;
bonusSummary: string[];
dropPolicyDescription: string;
}
export interface LeagueScoringConfigDTO {
leagueId: string;
seasonId: string;
gameId: string;
gameName: string;
scoringPresetId?: string;
scoringPresetName?: string;
dropPolicySummary: string;
championships: LeagueScoringChampionshipDTO[];
}

View File

@@ -0,0 +1,41 @@
export interface LeagueSummaryScoringDTO {
gameId: string;
gameName: string;
primaryChampionshipType: 'driver' | 'team' | 'nations' | 'trophy';
scoringPresetId: string;
scoringPresetName: string;
dropPolicySummary: string;
/**
* Human-readable scoring pattern summary combining preset name and drop policy,
* e.g. "Sprint + Main • Best 6 results of 8 count towards the championship."
*/
scoringPatternSummary: string;
}
export interface LeagueSummaryDTO {
id: string;
name: string;
description?: string;
createdAt: Date;
ownerId: string;
maxDrivers?: number;
usedDriverSlots?: number;
maxTeams?: number;
usedTeamSlots?: number;
/**
* Human-readable structure summary derived from capacity and (future) team settings,
* e.g. "Solo • 24 drivers" or "Teams • 12 × 2 drivers".
*/
structureSummary?: string;
/**
* Human-readable scoring pattern summary for list views,
* e.g. "Sprint + Main • Best 6 results of 8 count towards the championship."
*/
scoringPatternSummary?: string;
/**
* Human-readable timing summary for list views,
* e.g. "30 min Quali • 40 min Race".
*/
timingSummary?: string;
scoring?: LeagueSummaryScoringDTO;
}