view data fixes

This commit is contained in:
2026-01-23 15:30:23 +01:00
parent e22033be38
commit f8099f04bc
213 changed files with 3466 additions and 3003 deletions

View File

@@ -1,192 +1,79 @@
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 { ViewModel } from "../contracts/view-models/ViewModel";
import { RaceViewModel } from './RaceViewModel';
import { DriverViewModel } from './DriverViewModel';
import type { LeagueDetailPageViewData, LeagueMembershipWithRole, SponsorInfo } from '../view-data/LeagueDetailPageViewData';
// 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 }>;
}
import { ViewModel } from "../contracts/view-models/ViewModel";
export class LeagueDetailPageViewModel extends ViewModel {
// League basic info
id: string;
name: string;
description?: string;
ownerId: string;
createdAt: string;
settings: {
maxDrivers?: number;
};
socialLinks: {
discordUrl?: string;
youtubeUrl?: string;
websiteUrl?: string;
} | undefined;
private readonly data: LeagueDetailPageViewData;
readonly allRaces: RaceViewModel[];
readonly runningRaces: RaceViewModel[];
readonly ownerSummary: DriverSummary | null;
readonly adminSummaries: DriverSummary[];
readonly stewardSummaries: DriverSummary[];
// 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;
constructor(data: LeagueDetailPageViewData) {
super();
this.data = data;
// 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.allRaces = data.allRaces.map(r => r instanceof RaceViewModel ? r : new RaceViewModel(r));
this.runningRaces = this.allRaces.filter(r => r.status === 'running');
// Build driver summaries
this.ownerSummary = this.buildDriverSummary(data.ownerId);
this.adminSummaries = data.memberships
.filter(m => m.role === 'admin')
.slice(0, 3)
.map(m => this.buildDriverSummary(m.driverId))
.filter((s): s is DriverSummary => s !== null);
this.stewardSummaries = data.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 driverData = this.data.drivers.find(d => d.id === driverId) ||
(this.data.owner?.id === driverId ? this.data.owner : null);
if (!driverData) return null;
const driver = new DriverViewModel(driverData);
return {
driver,
rating: null,
rank: null,
};
}
this.owner = owner;
this.scoringConfig = scoringConfig;
this.drivers = drivers;
get id(): string { return this.data.id; }
get name(): string { return this.data.name; }
get description(): string { return this.data.description ?? ''; }
get ownerId(): string { return this.data.ownerId; }
get createdAt(): string { return this.data.createdAt; }
get settings() { return this.data.settings; }
get socialLinks() { return this.data.socialLinks; }
get owner() { return this.data.owner; }
get scoringConfig() { return this.data.scoringConfig; }
get drivers() { return this.data.drivers; }
get memberships() { return this.data.memberships; }
get averageSOF(): number | null { return this.data.averageSOF; }
get completedRacesCount(): number { return this.data.completedRacesCount; }
get sponsors(): SponsorInfo[] { return this.data.sponsors; }
// 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
get sponsorInsights() {
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 = {
return {
avgViewsPerRace: 5400 + memberCount * 50,
totalImpressions: 45000 + memberCount * 500,
engagementRate: (3.5 + (memberCount / 50)).toFixed(1),
@@ -200,59 +87,17 @@ export class LeagueDetailPageViewModel extends ViewModel {
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';
}
}
}