This commit is contained in:
2025-12-18 14:48:37 +01:00
parent b476bb7e99
commit f54fa5de5b
23 changed files with 699 additions and 543 deletions

View File

@@ -0,0 +1,26 @@
import { LeagueMembershipService } from '@/lib/services/leagues/LeagueMembershipService';
import { LeagueRoleUtility } from '@/lib/utilities/LeagueRoleUtility';
export class LeagueMembershipUtility {
/**
* Derive a driver's primary league from cached memberships.
* Prefers any active membership and returns the first matching league.
*/
static getPrimaryLeagueIdForDriver(driverId: string): string | null {
for (const [leagueId, members] of LeagueMembershipService.getCachedMembershipsIterator()) {
if (members.some((m) => m.driverId === driverId && m.status === 'active')) {
return leagueId;
}
}
return null;
}
/**
* Check if a driver is owner or admin of a league.
*/
static isOwnerOrAdmin(leagueId: string, driverId: string): boolean {
const membership = LeagueMembershipService.getMembership(leagueId, driverId);
if (!membership) return false;
return LeagueRoleUtility.isLeagueAdminOrHigherRole(membership.role);
}
}

View File

@@ -0,0 +1,40 @@
import { LeagueRole } from '@/lib/types/LeagueRole';
export class LeagueRoleUtility {
static isLeagueOwnerRole(role: LeagueRole): boolean {
return role === 'owner';
}
static isLeagueAdminRole(role: LeagueRole): boolean {
return role === 'admin';
}
static isLeagueStewardRole(role: LeagueRole): boolean {
return role === 'steward';
}
static isLeagueMemberRole(role: LeagueRole): boolean {
return role === 'member';
}
/**
* Returns true for roles that should be treated as having elevated permissions.
* This keeps UI logic open for future roles like steward, streamer, sponsor.
*/
static isLeagueAdminOrHigherRole(role: LeagueRole): boolean {
return role === 'owner' || role === 'admin' || role === 'steward';
}
/**
* Ordering helper for sorting memberships in tables.
*/
static getLeagueRoleOrder(role: LeagueRole): number {
const order: Record<LeagueRole, number> = {
owner: 0,
admin: 1,
steward: 2,
member: 3,
};
return order[role] ?? 99;
}
}

View File

@@ -0,0 +1,50 @@
// TODO: Move this business logic to core domain layer - scoring presets and their timing rules are domain concepts
type Timings = {
practiceMinutes?: number;
qualifyingMinutes?: number;
sprintRaceMinutes?: number;
mainRaceMinutes?: number;
sessionCount?: number;
roundsPlanned?: number;
raceDayOfWeek?: number;
raceTimeUtc?: string;
};
export class ScoringPresetApplier {
static applyToTimings(patternId: string, currentTimings: Timings): Timings {
const lowerPresetId = patternId.toLowerCase();
let updatedTimings: Timings = { ...currentTimings };
if (lowerPresetId.includes('sprint') || lowerPresetId.includes('double')) {
updatedTimings = {
...updatedTimings,
practiceMinutes: 15,
qualifyingMinutes: 20,
sprintRaceMinutes: 20,
mainRaceMinutes: 35,
sessionCount: 2,
};
} else if (lowerPresetId.includes('endurance') || lowerPresetId.includes('long')) {
updatedTimings = {
...updatedTimings,
practiceMinutes: 30,
qualifyingMinutes: 30,
mainRaceMinutes: 90,
sessionCount: 1,
};
delete (updatedTimings as { sprintRaceMinutes?: number }).sprintRaceMinutes;
} else {
updatedTimings = {
...updatedTimings,
practiceMinutes: 20,
qualifyingMinutes: 30,
mainRaceMinutes: 40,
sessionCount: 1,
};
delete (updatedTimings as { sprintRaceMinutes?: number }).sprintRaceMinutes;
}
return updatedTimings;
}
}