import { LeagueWithCapacityAndScoringDTO } from '@/lib/types/generated/LeagueWithCapacityAndScoringDTO'; import { LeagueStatsDTO } from '@/lib/types/generated/LeagueStatsDTO'; import { LeagueMembershipsDTO } from '@/lib/types/generated/LeagueMembershipsDTO'; import { GetDriverOutputDTO } from '@/lib/types/generated/GetDriverOutputDTO'; import { LeagueScoringConfigDTO } from '@/lib/types/generated/LeagueScoringConfigDTO'; import { RaceViewModel } from './RaceViewModel'; import { DriverViewModel } from './DriverViewModel'; // Sponsor info type export interface SponsorInfo { id: string; name: string; logoUrl?: string; websiteUrl?: string; tier: 'main' | 'secondary'; tagline?: string; } // Driver summary for management section export interface DriverSummary { driver: DriverViewModel; rating: number | null; rank: number | null; } // League membership with role export interface LeagueMembershipWithRole { driverId: string; role: 'owner' | 'admin' | 'steward' | 'member'; status: 'active' | 'inactive'; joinedAt: string; } // Helper interfaces for type narrowing interface LeagueSettings { maxDrivers?: number; } interface SocialLinks { discordUrl?: string; youtubeUrl?: string; websiteUrl?: string; } interface LeagueStatsExtended { averageSOF?: number; averageRating?: number; completedRaces?: number; totalRaces?: number; } interface MembershipsContainer { members?: Array<{ driverId: string; role: string; status?: 'active' | 'inactive'; joinedAt: string }>; memberships?: Array<{ driverId: string; role: string; status?: 'active' | 'inactive'; joinedAt: string }>; } export class LeagueDetailPageViewModel { // League basic info id: string; name: string; description?: string; ownerId: string; createdAt: string; settings: { maxDrivers?: number; }; socialLinks: { discordUrl?: string; youtubeUrl?: string; websiteUrl?: string; } | undefined; // Owner info owner: GetDriverOutputDTO | null; // Scoring configuration scoringConfig: LeagueScoringConfigDTO | null; // Drivers and memberships drivers: GetDriverOutputDTO[]; memberships: LeagueMembershipWithRole[]; // Races allRaces: RaceViewModel[]; runningRaces: RaceViewModel[]; // Stats averageSOF: number | null; completedRacesCount: number; // Sponsors sponsors: SponsorInfo[]; // Sponsor insights data sponsorInsights: { avgViewsPerRace: number; totalImpressions: number; engagementRate: string; estimatedReach: number; mainSponsorAvailable: boolean; secondarySlotsAvailable: number; mainSponsorPrice: number; secondaryPrice: number; tier: 'premium' | 'standard' | 'starter'; trustScore: number; discordMembers: number; monthlyActivity: number; }; // Driver summaries for management ownerSummary: DriverSummary | null; adminSummaries: DriverSummary[]; stewardSummaries: DriverSummary[]; constructor( league: LeagueWithCapacityAndScoringDTO, owner: GetDriverOutputDTO | null, scoringConfig: LeagueScoringConfigDTO | null, drivers: GetDriverOutputDTO[], memberships: LeagueMembershipsDTO, allRaces: RaceViewModel[], leagueStats: LeagueStatsDTO, sponsors: SponsorInfo[] ) { this.id = league.id; this.name = league.name; this.description = league.description ?? ''; this.ownerId = league.ownerId; this.createdAt = league.createdAt; // Handle settings with proper type narrowing const settings = league.settings as LeagueSettings | undefined; const maxDrivers = settings?.maxDrivers; this.settings = { maxDrivers: maxDrivers, }; // Handle social links with proper type narrowing const socialLinks = league.socialLinks as SocialLinks | undefined; const discordUrl = socialLinks?.discordUrl; const youtubeUrl = socialLinks?.youtubeUrl; const websiteUrl = socialLinks?.websiteUrl; this.socialLinks = { discordUrl, youtubeUrl, websiteUrl, }; this.owner = owner; this.scoringConfig = scoringConfig; this.drivers = drivers; // Handle memberships with proper type narrowing const membershipsContainer = memberships as MembershipsContainer; const membershipDtos = membershipsContainer.members ?? membershipsContainer.memberships ?? []; this.memberships = membershipDtos.map((m) => ({ driverId: m.driverId, role: m.role as 'owner' | 'admin' | 'steward' | 'member', status: m.status ?? 'active', joinedAt: m.joinedAt, })); this.allRaces = allRaces; this.runningRaces = allRaces.filter(r => r.status === 'running'); // Calculate SOF from available data with proper type narrowing const statsExtended = leagueStats as LeagueStatsExtended; const averageSOF = statsExtended.averageSOF ?? statsExtended.averageRating ?? undefined; const completedRaces = statsExtended.completedRaces ?? statsExtended.totalRaces ?? undefined; this.averageSOF = typeof averageSOF === 'number' ? averageSOF : null; this.completedRacesCount = typeof completedRaces === 'number' ? completedRaces : 0; this.sponsors = sponsors; // Calculate sponsor insights const memberCount = this.memberships.length; const mainSponsorTaken = this.sponsors.some(s => s.tier === 'main'); const secondaryTaken = this.sponsors.filter(s => s.tier === 'secondary').length; this.sponsorInsights = { avgViewsPerRace: 5400 + memberCount * 50, totalImpressions: 45000 + memberCount * 500, engagementRate: (3.5 + (memberCount / 50)).toFixed(1), estimatedReach: memberCount * 150, mainSponsorAvailable: !mainSponsorTaken, secondarySlotsAvailable: Math.max(0, 2 - secondaryTaken), mainSponsorPrice: 800 + Math.floor(memberCount * 10), secondaryPrice: 250 + Math.floor(memberCount * 3), tier: (this.averageSOF && this.averageSOF > 3000 ? 'premium' : this.averageSOF && this.averageSOF > 2000 ? 'standard' : 'starter') as 'premium' | 'standard' | 'starter', trustScore: Math.min(100, 60 + memberCount + this.completedRacesCount), discordMembers: memberCount * 3, monthlyActivity: Math.min(100, 40 + this.completedRacesCount * 2), }; // Build driver summaries this.ownerSummary = this.buildDriverSummary(this.ownerId); this.adminSummaries = this.memberships .filter(m => m.role === 'admin') .slice(0, 3) .map(m => this.buildDriverSummary(m.driverId)) .filter((s): s is DriverSummary => s !== null); this.stewardSummaries = this.memberships .filter(m => m.role === 'steward') .slice(0, 3) .map(m => this.buildDriverSummary(m.driverId)) .filter((s): s is DriverSummary => s !== null); } private buildDriverSummary(driverId: string): DriverSummary | null { const driverDto = this.drivers.find(d => d.id === driverId); if (!driverDto) return null; // Handle avatarUrl with proper type checking const driverAny = driverDto as { avatarUrl?: unknown }; const avatarUrl = typeof driverAny.avatarUrl === 'string' ? driverAny.avatarUrl : null; const driver = new DriverViewModel({ id: driverDto.id, name: driverDto.name, avatarUrl: avatarUrl, iracingId: driverDto.iracingId, }); // Detailed rating and rank data are not wired from the analytics services yet; // expose the driver identity only so the UI can still render role assignments. return { driver, rating: null, rank: null, }; } // UI helper methods get isSponsorMode(): boolean { // League detail pages are rendered in organizer mode in this build; sponsor-specific // mode switches will be introduced once sponsor dashboards share this view model. return false; } get currentUserMembership(): LeagueMembershipWithRole | null { // Current user identity is not available in this view model context yet; callers must // pass an explicit membership if they need per-user permissions. return null; } get canEndRaces(): boolean { return this.currentUserMembership?.role === 'admin' || this.currentUserMembership?.role === 'owner'; } }