305 lines
9.2 KiB
TypeScript
305 lines
9.2 KiB
TypeScript
import type { CreateLeagueInputDTO } from '@/lib/types/generated/CreateLeagueInputDTO';
|
|
import { LeagueWizardValidationMessages } from '@/lib/display-objects/LeagueWizardValidationMessages';
|
|
import { ScoringPresetApplier } from '@/lib/utilities/ScoringPresetApplier';
|
|
|
|
export type WizardStep = 1 | 2 | 3 | 4 | 5 | 6 | 7;
|
|
|
|
export interface WizardErrors {
|
|
basics?: {
|
|
name?: string;
|
|
description?: string;
|
|
visibility?: string;
|
|
};
|
|
structure?: {
|
|
maxDrivers?: string;
|
|
maxTeams?: string;
|
|
driversPerTeam?: string;
|
|
};
|
|
timings?: {
|
|
qualifyingMinutes?: string;
|
|
mainRaceMinutes?: string;
|
|
roundsPlanned?: string;
|
|
};
|
|
scoring?: {
|
|
patternId?: string;
|
|
};
|
|
submit?: string;
|
|
}
|
|
|
|
type LeagueWizardFormData = {
|
|
leagueId: string | undefined;
|
|
basics: {
|
|
name: string;
|
|
description?: string;
|
|
visibility: 'public' | 'private' | 'unlisted';
|
|
gameId: string;
|
|
};
|
|
structure: {
|
|
mode: 'solo' | 'fixedTeams';
|
|
maxDrivers?: number;
|
|
maxTeams?: number;
|
|
driversPerTeam?: number;
|
|
};
|
|
championships: {
|
|
enableDriverChampionship: boolean;
|
|
enableTeamChampionship: boolean;
|
|
enableNationsChampionship: boolean;
|
|
enableTrophyChampionship: boolean;
|
|
};
|
|
scoring: {
|
|
patternId?: string;
|
|
customScoringEnabled?: boolean;
|
|
};
|
|
dropPolicy: {
|
|
strategy: 'none' | 'bestNResults' | 'dropWorstN';
|
|
n?: number;
|
|
};
|
|
timings: {
|
|
practiceMinutes?: number;
|
|
qualifyingMinutes?: number;
|
|
sprintRaceMinutes?: number;
|
|
mainRaceMinutes?: number;
|
|
sessionCount?: number;
|
|
roundsPlanned?: number;
|
|
raceDayOfWeek?: number;
|
|
raceTimeUtc?: string;
|
|
};
|
|
stewarding: {
|
|
decisionMode: 'owner_only' | 'admin_vote' | 'steward_panel';
|
|
requiredVotes?: number;
|
|
requireDefense: boolean;
|
|
defenseTimeLimit: number;
|
|
voteTimeLimit: number;
|
|
protestDeadlineHours: number;
|
|
stewardingClosesHours: number;
|
|
notifyAccusedOnProtest: boolean;
|
|
notifyOnVoteRequired: boolean;
|
|
};
|
|
};
|
|
|
|
export class LeagueWizardCommandModel {
|
|
leagueId: string | undefined;
|
|
basics: LeagueWizardFormData['basics'];
|
|
structure: LeagueWizardFormData['structure'];
|
|
championships: LeagueWizardFormData['championships'];
|
|
scoring: LeagueWizardFormData['scoring'];
|
|
dropPolicy: LeagueWizardFormData['dropPolicy'];
|
|
timings: LeagueWizardFormData['timings'];
|
|
stewarding: LeagueWizardFormData['stewarding'];
|
|
|
|
constructor(initial: Partial<LeagueWizardFormData> = {}) {
|
|
this.leagueId = initial.leagueId;
|
|
this.basics = {
|
|
name: '',
|
|
description: '',
|
|
visibility: 'public',
|
|
gameId: '',
|
|
...initial.basics,
|
|
};
|
|
this.structure = {
|
|
mode: 'solo',
|
|
...initial.structure,
|
|
};
|
|
this.championships = {
|
|
enableDriverChampionship: true,
|
|
enableTeamChampionship: false,
|
|
enableNationsChampionship: false,
|
|
enableTrophyChampionship: false,
|
|
...initial.championships,
|
|
};
|
|
this.scoring = {
|
|
...initial.scoring,
|
|
};
|
|
this.dropPolicy = {
|
|
strategy: 'none',
|
|
...initial.dropPolicy,
|
|
};
|
|
this.timings = {
|
|
...initial.timings,
|
|
};
|
|
this.stewarding = {
|
|
decisionMode: 'owner_only',
|
|
requireDefense: false,
|
|
defenseTimeLimit: 24,
|
|
voteTimeLimit: 48,
|
|
protestDeadlineHours: 24,
|
|
stewardingClosesHours: 168,
|
|
notifyAccusedOnProtest: true,
|
|
notifyOnVoteRequired: true,
|
|
...initial.stewarding,
|
|
};
|
|
}
|
|
|
|
validateStep(step: WizardStep): WizardErrors {
|
|
const errors: WizardErrors = {};
|
|
|
|
// Step 1: Basics
|
|
if (step === 1) {
|
|
const basicsErrors: NonNullable<WizardErrors['basics']> = {};
|
|
|
|
if (!this.basics.name || this.basics.name.trim().length === 0) {
|
|
basicsErrors.name = LeagueWizardValidationMessages.LEAGUE_NAME_REQUIRED;
|
|
} else if (this.basics.name.length < 3) {
|
|
basicsErrors.name = LeagueWizardValidationMessages.LEAGUE_NAME_TOO_SHORT;
|
|
} else if (this.basics.name.length > 100) {
|
|
basicsErrors.name = LeagueWizardValidationMessages.LEAGUE_NAME_TOO_LONG;
|
|
}
|
|
|
|
if (this.basics.description && this.basics.description.length > 500) {
|
|
basicsErrors.description = LeagueWizardValidationMessages.DESCRIPTION_TOO_LONG;
|
|
}
|
|
|
|
if (Object.keys(basicsErrors).length > 0) {
|
|
errors.basics = basicsErrors;
|
|
}
|
|
}
|
|
|
|
// Step 2: Visibility
|
|
if (step === 2) {
|
|
const basicsErrors: NonNullable<WizardErrors['basics']> = {};
|
|
|
|
if (!this.basics.visibility) {
|
|
basicsErrors.visibility = LeagueWizardValidationMessages.VISIBILITY_REQUIRED;
|
|
}
|
|
|
|
if (Object.keys(basicsErrors).length > 0) {
|
|
errors.basics = basicsErrors;
|
|
}
|
|
}
|
|
|
|
// Step 3: Structure
|
|
if (step === 3) {
|
|
const structureErrors: NonNullable<WizardErrors['structure']> = {};
|
|
|
|
if (this.structure.mode === 'solo') {
|
|
if (!this.structure.maxDrivers || this.structure.maxDrivers <= 0) {
|
|
structureErrors.maxDrivers = LeagueWizardValidationMessages.MAX_DRIVERS_INVALID_SOLO;
|
|
} else if (this.structure.maxDrivers > 100) {
|
|
structureErrors.maxDrivers = LeagueWizardValidationMessages.MAX_DRIVERS_TOO_HIGH;
|
|
}
|
|
} else if (this.structure.mode === 'fixedTeams') {
|
|
if (!this.structure.maxTeams || this.structure.maxTeams <= 0) {
|
|
structureErrors.maxTeams = LeagueWizardValidationMessages.MAX_TEAMS_INVALID_TEAM;
|
|
}
|
|
if (!this.structure.driversPerTeam || this.structure.driversPerTeam <= 0) {
|
|
structureErrors.driversPerTeam = LeagueWizardValidationMessages.DRIVERS_PER_TEAM_INVALID;
|
|
}
|
|
}
|
|
if (Object.keys(structureErrors).length > 0) {
|
|
errors.structure = structureErrors;
|
|
}
|
|
}
|
|
|
|
// Step 4: Timings
|
|
if (step === 4) {
|
|
const timingsErrors: NonNullable<WizardErrors['timings']> = {};
|
|
if (!this.timings.qualifyingMinutes || this.timings.qualifyingMinutes <= 0) {
|
|
timingsErrors.qualifyingMinutes = LeagueWizardValidationMessages.QUALIFYING_DURATION_INVALID;
|
|
}
|
|
if (!this.timings.mainRaceMinutes || this.timings.mainRaceMinutes <= 0) {
|
|
timingsErrors.mainRaceMinutes = LeagueWizardValidationMessages.MAIN_RACE_DURATION_INVALID;
|
|
}
|
|
if (Object.keys(timingsErrors).length > 0) {
|
|
errors.timings = timingsErrors;
|
|
}
|
|
}
|
|
|
|
// Step 5: Scoring
|
|
if (step === 5) {
|
|
const scoringErrors: NonNullable<WizardErrors['scoring']> = {};
|
|
if (!this.scoring.patternId && !this.scoring.customScoringEnabled) {
|
|
scoringErrors.patternId = LeagueWizardValidationMessages.SCORING_PRESET_OR_CUSTOM_REQUIRED;
|
|
}
|
|
if (Object.keys(scoringErrors).length > 0) {
|
|
errors.scoring = scoringErrors;
|
|
}
|
|
}
|
|
|
|
return errors;
|
|
}
|
|
|
|
validateAll(): WizardErrors {
|
|
const aggregate: WizardErrors = {};
|
|
|
|
const merge = (next: WizardErrors) => {
|
|
if (next.basics) {
|
|
aggregate.basics = { ...aggregate.basics, ...next.basics };
|
|
}
|
|
if (next.structure) {
|
|
aggregate.structure = { ...aggregate.structure, ...next.structure };
|
|
}
|
|
if (next.timings) {
|
|
aggregate.timings = { ...aggregate.timings, ...next.timings };
|
|
}
|
|
if (next.scoring) {
|
|
aggregate.scoring = { ...aggregate.scoring, ...next.scoring };
|
|
}
|
|
if (next.submit) {
|
|
aggregate.submit = next.submit;
|
|
}
|
|
};
|
|
|
|
merge(this.validateStep(1));
|
|
merge(this.validateStep(2));
|
|
merge(this.validateStep(3));
|
|
merge(this.validateStep(4));
|
|
merge(this.validateStep(5));
|
|
|
|
return aggregate;
|
|
}
|
|
|
|
static hasWizardErrors(errors: WizardErrors): boolean {
|
|
return Object.keys(errors).some((key) => {
|
|
const value = errors[key as keyof WizardErrors];
|
|
if (!value) return false;
|
|
if (typeof value === 'string') return true;
|
|
return Object.keys(value).length > 0;
|
|
});
|
|
}
|
|
|
|
applyScoringPreset(patternId: string): void {
|
|
this.scoring = {
|
|
...this.scoring,
|
|
patternId,
|
|
customScoringEnabled: false,
|
|
};
|
|
this.timings = ScoringPresetApplier.applyToTimings(patternId, this.timings);
|
|
}
|
|
|
|
toCreateLeagueCommand(ownerId: string): CreateLeagueInputDTO {
|
|
return {
|
|
name: this.basics.name.trim(),
|
|
description: this.basics.description?.trim() ?? '',
|
|
// API currently only supports public/private. Treat unlisted as private for now.
|
|
visibility: this.basics.visibility === 'public' ? 'public' : 'private',
|
|
ownerId,
|
|
};
|
|
}
|
|
|
|
// Static methods for backward compatibility with component usage
|
|
static validateLeagueWizardStep(form: LeagueWizardFormData, step: WizardStep): WizardErrors {
|
|
const instance = new LeagueWizardCommandModel(form);
|
|
return instance.validateStep(step);
|
|
}
|
|
|
|
static validateAllLeagueWizardSteps(form: LeagueWizardFormData): WizardErrors {
|
|
const instance = new LeagueWizardCommandModel(form);
|
|
return instance.validateAll();
|
|
}
|
|
|
|
static applyScoringPresetToConfig(form: LeagueWizardFormData, patternId: string): LeagueWizardFormData {
|
|
const instance = new LeagueWizardCommandModel(form);
|
|
instance.applyScoringPreset(patternId);
|
|
return {
|
|
leagueId: instance.leagueId,
|
|
basics: instance.basics,
|
|
structure: instance.structure,
|
|
championships: instance.championships,
|
|
scoring: instance.scoring,
|
|
dropPolicy: instance.dropPolicy,
|
|
timings: instance.timings,
|
|
stewarding: instance.stewarding,
|
|
};
|
|
}
|
|
}
|