api client refactor

This commit is contained in:
2025-12-17 18:01:47 +01:00
parent bab55955e1
commit 4177644b18
190 changed files with 6403 additions and 1624 deletions

View File

@@ -0,0 +1,30 @@
// Analytics dashboard view model
// Represents dashboard data for analytics
export class AnalyticsDashboardViewModel {
totalUsers: number;
activeUsers: number;
totalRaces: number;
totalLeagues: number;
constructor(data: { totalUsers: number; activeUsers: number; totalRaces: number; totalLeagues: number }) {
Object.assign(this, data);
}
/** UI-specific: User engagement rate */
get userEngagementRate(): number {
return this.totalUsers > 0 ? (this.activeUsers / this.totalUsers) * 100 : 0;
}
/** UI-specific: Formatted engagement rate */
get formattedEngagementRate(): string {
return `${this.userEngagementRate.toFixed(1)}%`;
}
/** UI-specific: Activity level */
get activityLevel(): string {
if (this.userEngagementRate > 70) return 'High';
if (this.userEngagementRate > 40) return 'Medium';
return 'Low';
}
}

View File

@@ -0,0 +1,35 @@
// Analytics metrics view model
// Represents metrics data for analytics
export class AnalyticsMetricsViewModel {
pageViews: number;
uniqueVisitors: number;
averageSessionDuration: number;
bounceRate: number;
constructor(data: { pageViews: number; uniqueVisitors: number; averageSessionDuration: number; bounceRate: number }) {
Object.assign(this, data);
}
/** UI-specific: Formatted page views */
get formattedPageViews(): string {
return this.pageViews.toLocaleString();
}
/** UI-specific: Formatted unique visitors */
get formattedUniqueVisitors(): string {
return this.uniqueVisitors.toLocaleString();
}
/** UI-specific: Formatted session duration */
get formattedSessionDuration(): string {
const minutes = Math.floor(this.averageSessionDuration / 60);
const seconds = Math.floor(this.averageSessionDuration % 60);
return `${minutes}:${seconds.toString().padStart(2, '0')}`;
}
/** UI-specific: Formatted bounce rate */
get formattedBounceRate(): string {
return `${this.bounceRate.toFixed(1)}%`;
}
}

View File

@@ -0,0 +1,76 @@
import { DriverLeaderboardItemDto } from '../dtos';
export class DriverLeaderboardItemViewModel implements DriverLeaderboardItemDto {
id: string;
name: string;
avatarUrl?: string;
rating: number;
wins: number;
races: number;
skillLevel: string;
isActive: boolean;
nationality: string;
podiums: number;
position: number;
private previousRating?: number;
constructor(dto: DriverLeaderboardItemDto, position: number, previousRating?: number) {
Object.assign(this, dto);
this.position = position;
this.previousRating = previousRating;
}
/** UI-specific: Skill level color */
get skillLevelColor(): string {
switch (this.skillLevel) {
case 'beginner': return 'green';
case 'intermediate': return 'yellow';
case 'advanced': return 'orange';
case 'expert': return 'red';
default: return 'gray';
}
}
/** UI-specific: Skill level icon */
get skillLevelIcon(): string {
switch (this.skillLevel) {
case 'beginner': return '🥉';
case 'intermediate': return '🥈';
case 'advanced': return '🥇';
case 'expert': return '👑';
default: return '🏁';
}
}
/** UI-specific: Win rate */
get winRate(): number {
return this.races > 0 ? (this.wins / this.races) * 100 : 0;
}
/** UI-specific: Formatted win rate */
get winRateFormatted(): string {
return `${this.winRate.toFixed(1)}%`;
}
/** UI-specific: Rating trend */
get ratingTrend(): 'up' | 'down' | 'same' {
if (!this.previousRating) return 'same';
if (this.rating > this.previousRating) return 'up';
if (this.rating < this.previousRating) return 'down';
return 'same';
}
/** UI-specific: Rating change indicator */
get ratingChangeIndicator(): string {
const change = this.previousRating ? this.rating - this.previousRating : 0;
if (change > 0) return `+${change}`;
if (change < 0) return `${change}`;
return '0';
}
/** UI-specific: Position badge */
get positionBadge(): string {
return this.position.toString();
}
}

View File

@@ -0,0 +1,28 @@
import { DriversLeaderboardDto, DriverLeaderboardItemDto } from '../dtos';
import { DriverLeaderboardItemViewModel } from './DriverLeaderboardItemViewModel';
export class DriverLeaderboardViewModel implements DriversLeaderboardDto {
drivers: DriverLeaderboardItemViewModel[];
constructor(dto: DriversLeaderboardDto & { drivers: DriverLeaderboardItemDto[] }, previousDrivers?: DriverLeaderboardItemDto[]) {
this.drivers = dto.drivers.map((driver, index) => {
const previous = previousDrivers?.find(p => p.id === driver.id);
return new DriverLeaderboardItemViewModel(driver, index + 1, previous?.rating);
});
}
/** UI-specific: Total races across all drivers */
get totalRaces(): number {
return this.drivers.reduce((sum, driver) => sum + driver.races, 0);
}
/** UI-specific: Total wins across all drivers */
get totalWins(): number {
return this.drivers.reduce((sum, driver) => sum + driver.wins, 0);
}
/** UI-specific: Active drivers count */
get activeCount(): number {
return this.drivers.filter(driver => driver.isActive).length;
}
}

View File

@@ -0,0 +1,36 @@
import { DriverRegistrationStatusDto } from '../dtos';
export class DriverRegistrationStatusViewModel implements DriverRegistrationStatusDto {
isRegistered: boolean;
raceId: string;
driverId: string;
constructor(dto: DriverRegistrationStatusDto) {
Object.assign(this, dto);
}
/** UI-specific: Status message */
get statusMessage(): string {
return this.isRegistered ? 'Registered for this race' : 'Not registered';
}
/** UI-specific: Status color */
get statusColor(): string {
return this.isRegistered ? 'green' : 'red';
}
/** UI-specific: Badge variant */
get statusBadgeVariant(): string {
return this.isRegistered ? 'success' : 'warning';
}
/** UI-specific: Registration button text */
get registrationButtonText(): string {
return this.isRegistered ? 'Withdraw' : 'Register';
}
/** UI-specific: Whether can register (assuming always can if not registered) */
get canRegister(): boolean {
return !this.isRegistered;
}
}

View File

@@ -0,0 +1,16 @@
import type { LeagueAdminDto } from '../dtos';
import type { LeagueMemberViewModel, LeagueJoinRequestViewModel } from './';
/**
* League admin view model
* Transform from DTO to ViewModel with UI fields
*/
export interface LeagueAdminViewModel {
config: LeagueAdminDto['config'];
members: LeagueMemberViewModel[];
joinRequests: LeagueJoinRequestViewModel[];
// Total pending requests count
pendingRequestsCount: number;
// Whether there are any pending requests
hasPendingRequests: boolean;
}

View File

@@ -0,0 +1,14 @@
import type { LeagueJoinRequestDto } from '../dtos';
/**
* League join request view model
* Transform from DTO to ViewModel with UI fields
*/
export interface LeagueJoinRequestViewModel extends LeagueJoinRequestDto {
// Formatted request date
formattedRequestedAt: string;
// Whether the request can be approved by current user
canApprove: boolean;
// Whether the request can be rejected by current user
canReject: boolean;
}

View File

@@ -0,0 +1,39 @@
import { LeagueMemberDto, DriverDto } from '../dtos';
export class LeagueMemberViewModel implements LeagueMemberDto {
driverId: string;
driver?: DriverDto;
role: string;
joinedAt: string;
private currentUserId: string;
constructor(dto: LeagueMemberDto, currentUserId: string) {
Object.assign(this, dto);
this.currentUserId = currentUserId;
}
/** UI-specific: Formatted join date */
get formattedJoinedAt(): string {
return new Date(this.joinedAt).toLocaleDateString();
}
/** UI-specific: Badge variant for role */
get roleBadgeVariant(): string {
switch (this.role) {
case 'owner': return 'primary';
case 'admin': return 'secondary';
default: return 'default';
}
}
/** UI-specific: Whether this member is the owner */
get isOwner(): boolean {
return this.role === 'owner';
}
/** UI-specific: Whether this is the current user */
get isCurrentUser(): boolean {
return this.driverId === this.currentUserId;
}
}

View File

@@ -0,0 +1,19 @@
import { LeagueStandingsDto, StandingEntryDto, DriverDto, LeagueMembership } from '../dtos';
import { StandingEntryViewModel } from './StandingEntryViewModel';
export class LeagueStandingsViewModel implements LeagueStandingsDto {
standings: StandingEntryViewModel[];
drivers: DriverDto[];
memberships: LeagueMembership[];
constructor(dto: LeagueStandingsDto & { standings: StandingEntryDto[] }, currentUserId: string, previousStandings?: StandingEntryDto[]) {
const leaderPoints = dto.standings[0]?.points || 0;
this.standings = dto.standings.map((entry, index) => {
const nextPoints = dto.standings[index + 1]?.points || entry.points;
const previousPosition = previousStandings?.find(p => p.driverId === entry.driverId)?.position;
return new StandingEntryViewModel(entry, leaderPoints, nextPoints, currentUserId, previousPosition);
});
this.drivers = dto.drivers;
this.memberships = dto.memberships;
}
}

View File

@@ -0,0 +1,63 @@
import { LeagueSummaryDto } from '../dtos';
export class LeagueSummaryViewModel implements LeagueSummaryDto {
id: string;
name: string;
description?: string;
logoUrl?: string;
coverImage?: string;
memberCount: number;
maxMembers: number;
isPublic: boolean;
ownerId: string;
ownerName?: string;
scoringType?: string;
status?: string;
constructor(dto: LeagueSummaryDto) {
Object.assign(this, dto);
}
/** UI-specific: Formatted capacity display */
get formattedCapacity(): string {
return `${this.memberCount}/${this.maxMembers}`;
}
/** UI-specific: Capacity bar percentage */
get capacityBarPercent(): number {
return (this.memberCount / this.maxMembers) * 100;
}
/** UI-specific: Label for join button */
get joinButtonLabel(): string {
if (this.isFull) return 'Full';
return this.isJoinable ? 'Join League' : 'Request to Join';
}
/** UI-specific: Whether the league is full */
get isFull(): boolean {
return this.memberCount >= this.maxMembers;
}
/** UI-specific: Whether the league is joinable */
get isJoinable(): boolean {
return this.isPublic && !this.isFull;
}
/** UI-specific: Color for member progress */
get memberProgressColor(): string {
const percent = this.capacityBarPercent;
if (percent < 50) return 'green';
if (percent < 80) return 'yellow';
return 'red';
}
/** UI-specific: Badge variant for status */
get statusBadgeVariant(): string {
switch (this.status) {
case 'active': return 'success';
case 'inactive': return 'secondary';
default: return 'default';
}
}
}

View File

@@ -0,0 +1,32 @@
import { MembershipFeeDto } from '../dtos';
export class MembershipFeeViewModel implements MembershipFeeDto {
leagueId: string;
amount: number;
currency: string;
period: string;
constructor(dto: MembershipFeeDto) {
Object.assign(this, dto);
}
/** UI-specific: Formatted amount */
get formattedAmount(): string {
return `${this.currency} ${this.amount.toFixed(2)}`;
}
/** UI-specific: Period display */
get periodDisplay(): string {
switch (this.period) {
case 'monthly': return 'Monthly';
case 'yearly': return 'Yearly';
case 'season': return 'Per Season';
default: return this.period;
}
}
/** UI-specific: Amount per period */
get amountPerPeriod(): string {
return `${this.formattedAmount} ${this.periodDisplay.toLowerCase()}`;
}
}

View File

@@ -0,0 +1,38 @@
import { PaymentDto } from '../dtos';
export class PaymentViewModel implements PaymentDto {
id: string;
amount: number;
currency: string;
status: string;
createdAt: string;
constructor(dto: PaymentDto) {
Object.assign(this, dto);
}
/** UI-specific: Formatted amount */
get formattedAmount(): string {
return `${this.currency} ${this.amount.toFixed(2)}`;
}
/** UI-specific: Status color */
get statusColor(): string {
switch (this.status) {
case 'completed': return 'green';
case 'pending': return 'yellow';
case 'failed': return 'red';
default: return 'gray';
}
}
/** UI-specific: Formatted created date */
get formattedCreatedAt(): string {
return new Date(this.createdAt).toLocaleString();
}
/** UI-specific: Status display */
get statusDisplay(): string {
return this.status.charAt(0).toUpperCase() + this.status.slice(1);
}
}

View File

@@ -0,0 +1,34 @@
import { PrizeDto } from '../dtos';
export class PrizeViewModel implements PrizeDto {
id: string;
name: string;
amount: number;
currency: string;
position?: number;
constructor(dto: PrizeDto) {
Object.assign(this, dto);
}
/** UI-specific: Formatted amount */
get formattedAmount(): string {
return `${this.currency} ${this.amount.toFixed(2)}`;
}
/** UI-specific: Position display */
get positionDisplay(): string {
if (!this.position) return 'Special';
switch (this.position) {
case 1: return '1st Place';
case 2: return '2nd Place';
case 3: return '3rd Place';
default: return `${this.position}th Place`;
}
}
/** UI-specific: Prize description */
get prizeDescription(): string {
return `${this.name} - ${this.formattedAmount}`;
}
}

View File

@@ -0,0 +1,13 @@
/**
* Protest view model
* Represents a race protest
*/
export interface ProtestViewModel {
id: string;
raceId: string;
complainantId: string;
defendantId: string;
description: string;
status: string;
createdAt: string;
}

View File

@@ -0,0 +1,57 @@
import { RaceDetailDto, RaceDetailRaceDto, RaceDetailLeagueDto, RaceDetailEntryDto, RaceDetailRegistrationDto, RaceDetailUserResultDto } from '../dtos';
export class RaceDetailViewModel implements RaceDetailDto {
race: RaceDetailRaceDto | null;
league: RaceDetailLeagueDto | null;
entryList: RaceDetailEntryDto[];
registration: RaceDetailRegistrationDto;
userResult: RaceDetailUserResultDto | null;
error?: string;
constructor(dto: RaceDetailDto) {
Object.assign(this, dto);
}
/** UI-specific: Whether user is registered */
get isRegistered(): boolean {
return this.registration.isRegistered;
}
/** UI-specific: Whether user can register */
get canRegister(): boolean {
return this.registration.canRegister;
}
/** UI-specific: Race status display */
get raceStatusDisplay(): string {
if (!this.race) return 'Unknown';
switch (this.race.status) {
case 'upcoming': return 'Upcoming';
case 'live': return 'Live';
case 'finished': return 'Finished';
default: return this.race.status;
}
}
/** UI-specific: Formatted scheduled time */
get formattedScheduledTime(): string {
return this.race ? new Date(this.race.scheduledTime).toLocaleString() : '';
}
/** UI-specific: Entry list count */
get entryCount(): number {
return this.entryList.length;
}
/** UI-specific: Whether race has results */
get hasResults(): boolean {
return this.userResult !== null;
}
/** UI-specific: Registration status message */
get registrationStatusMessage(): string {
if (this.isRegistered) return 'You are registered for this race';
if (this.canRegister) return 'You can register for this race';
return 'Registration not available';
}
}

View File

@@ -0,0 +1,55 @@
import { RaceListItemDto } from '../dtos';
export class RaceListItemViewModel implements RaceListItemDto {
id: string;
name: string;
leagueId: string;
leagueName: string;
scheduledTime: string;
status: string;
trackName?: string;
constructor(dto: RaceListItemDto) {
Object.assign(this, dto);
}
/** UI-specific: Formatted scheduled time */
get formattedScheduledTime(): string {
return new Date(this.scheduledTime).toLocaleString();
}
/** UI-specific: Badge variant for status */
get statusBadgeVariant(): string {
switch (this.status) {
case 'upcoming': return 'info';
case 'live': return 'success';
case 'finished': return 'secondary';
default: return 'default';
}
}
/** UI-specific: Whether race is upcoming */
get isUpcoming(): boolean {
return this.status === 'upcoming';
}
/** UI-specific: Whether race is live */
get isLive(): boolean {
return this.status === 'live';
}
/** UI-specific: Time until start in minutes */
get timeUntilStart(): number {
const now = new Date();
const scheduled = new Date(this.scheduledTime);
return Math.max(0, Math.floor((scheduled.getTime() - now.getTime()) / (1000 * 60)));
}
/** UI-specific: Display for time until start */
get timeUntilStartDisplay(): string {
const minutes = this.timeUntilStart;
if (minutes < 60) return `${minutes}m`;
const hours = Math.floor(minutes / 60);
return `${hours}h ${minutes % 60}m`;
}
}

View File

@@ -0,0 +1,70 @@
import { RaceResultDto } from '../dtos';
export class RaceResultViewModel implements RaceResultDto {
id: string;
raceId: string;
driverId: string;
driverName: string;
avatarUrl: string;
position: number;
startPosition: number;
incidents: number;
fastestLap: number;
positionChange: number;
isPodium: boolean;
isClean: boolean;
constructor(dto: RaceResultDto) {
Object.assign(this, dto);
}
/** UI-specific: Display for position change */
get positionChangeDisplay(): string {
if (this.positionChange > 0) return `+${this.positionChange}`;
if (this.positionChange < 0) return `${this.positionChange}`;
return '0';
}
/** UI-specific: Color for position change */
get positionChangeColor(): string {
if (this.positionChange > 0) return 'green';
if (this.positionChange < 0) return 'red';
return 'gray';
}
/** UI-specific: Whether this is the winner */
get isWinner(): boolean {
return this.position === 1;
}
/** UI-specific: Whether has fastest lap */
get hasFastestLap(): boolean {
return this.fastestLap > 0;
}
/** UI-specific: Badge for position */
get positionBadge(): string {
return this.position.toString();
}
/** UI-specific: Color for incidents badge */
get incidentsBadgeColor(): string {
if (this.incidents === 0) return 'green';
if (this.incidents <= 2) return 'yellow';
return 'red';
}
/** UI-specific: Formatted lap time */
get lapTimeFormatted(): string {
if (this.fastestLap <= 0) return '--:--.---';
const minutes = Math.floor(this.fastestLap / 60);
const seconds = Math.floor(this.fastestLap % 60);
const milliseconds = Math.floor((this.fastestLap % 1) * 1000);
return `${minutes}:${seconds.toString().padStart(2, '0')}.${milliseconds.toString().padStart(3, '0')}`;
}
/** Compatibility with old DTO interface */
getPositionChange(): number {
return this.positionChange;
}
}

View File

@@ -0,0 +1,63 @@
import { RaceResultsDetailDto, RaceResultDto } from '../dtos';
import { RaceResultViewModel } from './RaceResultViewModel';
export class RaceResultsDetailViewModel implements RaceResultsDetailDto {
raceId: string;
track: string;
results: RaceResultViewModel[];
league?: { id: string; name: string };
race?: { id: string; track: string; scheduledAt: string };
drivers: { id: string; name: string }[];
pointsSystem: Record<number, number>;
fastestLapTime: number;
penalties: { driverId: string; type: string; value?: number }[];
currentDriverId: string;
private currentUserId: string;
constructor(dto: RaceResultsDetailDto & { results: RaceResultDto[] }, currentUserId: string) {
this.raceId = dto.raceId;
this.track = dto.track;
this.results = dto.results.map(r => new RaceResultViewModel({ ...r, raceId: dto.raceId }));
this.league = dto.league;
this.race = dto.race;
this.drivers = dto.drivers;
this.pointsSystem = dto.pointsSystem;
this.fastestLapTime = dto.fastestLapTime;
this.penalties = dto.penalties;
this.currentDriverId = dto.currentDriverId;
this.currentUserId = currentUserId;
}
/** UI-specific: Results sorted by position */
get resultsByPosition(): RaceResultViewModel[] {
return [...this.results].sort((a, b) => a.position - b.position);
}
/** UI-specific: Results sorted by fastest lap */
get resultsByFastestLap(): RaceResultViewModel[] {
return [...this.results].sort((a, b) => a.fastestLap - b.fastestLap);
}
/** UI-specific: Clean drivers only */
get cleanDrivers(): RaceResultViewModel[] {
return this.results.filter(r => r.isClean);
}
/** UI-specific: Current user's result */
get currentUserResult(): RaceResultViewModel | undefined {
return this.results.find(r => r.driverId === this.currentUserId);
}
/** UI-specific: Race stats */
get stats(): { totalDrivers: number; cleanRate: number; averageIncidents: number } {
const total = this.results.length;
const clean = this.cleanDrivers.length;
const totalIncidents = this.results.reduce((sum, r) => sum + r.incidents, 0);
return {
totalDrivers: total,
cleanRate: total > 0 ? (clean / total) * 100 : 0,
averageIncidents: total > 0 ? totalIncidents / total : 0
};
}
}

View File

@@ -0,0 +1,36 @@
import { SessionDataDto } from '../dtos';
export class SessionViewModel implements SessionDataDto {
userId: string;
email: string;
displayName?: string;
driverId?: string;
isAuthenticated: boolean;
constructor(dto: SessionDataDto) {
Object.assign(this, dto);
}
/** UI-specific: User greeting */
get greeting(): string {
return `Hello, ${this.displayName || this.email}!`;
}
/** UI-specific: Avatar initials */
get avatarInitials(): string {
if (this.displayName) {
return this.displayName.split(' ').map(n => n[0]).join('').toUpperCase();
}
return this.email[0].toUpperCase();
}
/** UI-specific: Whether has driver profile */
get hasDriverProfile(): boolean {
return !!this.driverId;
}
/** UI-specific: Authentication status display */
get authStatusDisplay(): string {
return this.isAuthenticated ? 'Logged In' : 'Logged Out';
}
}

View File

@@ -0,0 +1,27 @@
import { SponsorDto } from '../dtos';
export class SponsorViewModel implements SponsorDto {
id: string;
name: string;
logoUrl?: string;
websiteUrl?: string;
constructor(dto: SponsorDto) {
Object.assign(this, dto);
}
/** UI-specific: Display name */
get displayName(): string {
return this.name;
}
/** UI-specific: Whether has website */
get hasWebsite(): boolean {
return !!this.websiteUrl;
}
/** UI-specific: Website link text */
get websiteLinkText(): string {
return 'Visit Website';
}
}

View File

@@ -0,0 +1,41 @@
import { SponsorshipDetailDto } from '../dtos';
export class SponsorshipDetailViewModel implements SponsorshipDetailDto {
id: string;
leagueId: string;
leagueName: string;
seasonId: string;
tier: 'main' | 'secondary';
status: string;
amount: number;
currency: string;
constructor(dto: SponsorshipDetailDto) {
Object.assign(this, dto);
}
/** UI-specific: Formatted amount */
get formattedAmount(): string {
return `${this.currency} ${this.amount.toLocaleString()}`;
}
/** UI-specific: Tier badge variant */
get tierBadgeVariant(): string {
return this.tier === 'main' ? 'primary' : 'secondary';
}
/** UI-specific: Status color */
get statusColor(): string {
switch (this.status) {
case 'active': return 'green';
case 'pending': return 'yellow';
case 'expired': return 'red';
default: return 'gray';
}
}
/** UI-specific: Status display */
get statusDisplay(): string {
return this.status.charAt(0).toUpperCase() + this.status.slice(1);
}
}

View File

@@ -0,0 +1,61 @@
import { StandingEntryDto, DriverDto } from '../dtos';
export class StandingEntryViewModel implements StandingEntryDto {
driverId: string;
driver?: DriverDto;
position: number;
points: number;
wins: number;
podiums: number;
races: number;
private leaderPoints: number;
private nextPoints: number;
private currentUserId: string;
private previousPosition?: number;
constructor(dto: StandingEntryDto, leaderPoints: number, nextPoints: number, currentUserId: string, previousPosition?: number) {
Object.assign(this, dto);
this.leaderPoints = leaderPoints;
this.nextPoints = nextPoints;
this.currentUserId = currentUserId;
this.previousPosition = previousPosition;
}
/** UI-specific: Badge for position display */
get positionBadge(): string {
return this.position.toString();
}
/** UI-specific: Points difference to leader */
get pointsGapToLeader(): number {
return this.points - this.leaderPoints;
}
/** UI-specific: Points difference to next position */
get pointsGapToNext(): number {
return this.points - this.nextPoints;
}
/** UI-specific: Whether this entry is the current user */
get isCurrentUser(): boolean {
return this.driverId === this.currentUserId;
}
/** UI-specific: Trend compared to previous */
get trend(): 'up' | 'down' | 'same' {
if (!this.previousPosition) return 'same';
if (this.position < this.previousPosition) return 'up';
if (this.position > this.previousPosition) return 'down';
return 'same';
}
/** UI-specific: Arrow for trend */
get trendArrow(): string {
switch (this.trend) {
case 'up': return '↑';
case 'down': return '↓';
default: return '-';
}
}
}

View File

@@ -0,0 +1,47 @@
import { TeamDetailsDto, TeamMemberDto } from '../dtos';
import { TeamMemberViewModel } from './TeamMemberViewModel';
export class TeamDetailsViewModel implements TeamDetailsDto {
id: string;
name: string;
description?: string;
logoUrl?: string;
memberCount: number;
ownerId: string;
members: TeamMemberViewModel[];
private currentUserId: string;
constructor(dto: TeamDetailsDto & { members: TeamMemberDto[] }, currentUserId: string) {
this.id = dto.id;
this.name = dto.name;
this.description = dto.description;
this.logoUrl = dto.logoUrl;
this.memberCount = dto.memberCount;
this.ownerId = dto.ownerId;
this.members = dto.members.map(m => new TeamMemberViewModel(m, currentUserId, dto.ownerId));
this.currentUserId = currentUserId;
}
/** UI-specific: Whether current user is owner */
get isOwner(): boolean {
return this.currentUserId === this.ownerId;
}
/** UI-specific: Whether can add members */
get canAddMembers(): boolean {
return this.isOwner && this.memberCount < 10; // Assuming max 10
}
/** UI-specific: Member management actions available */
get memberActionsAvailable(): boolean {
return this.isOwner;
}
/** UI-specific: Team status */
get teamStatus(): string {
if (this.memberCount < 5) return 'Recruiting';
if (this.memberCount < 10) return 'Active';
return 'Full';
}
}

View File

@@ -0,0 +1,48 @@
import { TeamJoinRequestItemDto } from '../dtos';
export class TeamJoinRequestViewModel implements TeamJoinRequestItemDto {
id: string;
teamId: string;
driverId: string;
requestedAt: string;
message?: string;
private currentUserId: string;
private isOwner: boolean;
constructor(dto: TeamJoinRequestItemDto, currentUserId: string, isOwner: boolean) {
Object.assign(this, dto);
this.currentUserId = currentUserId;
this.isOwner = isOwner;
}
/** UI-specific: Whether current user can approve */
get canApprove(): boolean {
return this.isOwner;
}
/** UI-specific: Formatted requested date */
get formattedRequestedAt(): string {
return new Date(this.requestedAt).toLocaleString();
}
/** UI-specific: Request status (pending) */
get status(): string {
return 'Pending';
}
/** UI-specific: Status color */
get statusColor(): string {
return 'yellow';
}
/** UI-specific: Approve button text */
get approveButtonText(): string {
return 'Approve';
}
/** UI-specific: Reject button text */
get rejectButtonText(): string {
return 'Reject';
}
}

View File

@@ -0,0 +1,47 @@
import { TeamMemberDto, DriverDto } from '../dtos';
export class TeamMemberViewModel implements TeamMemberDto {
driverId: string;
driver?: DriverDto;
role: string;
joinedAt: string;
private currentUserId: string;
private teamOwnerId: string;
constructor(dto: TeamMemberDto, currentUserId: string, teamOwnerId: string) {
Object.assign(this, dto);
this.currentUserId = currentUserId;
this.teamOwnerId = teamOwnerId;
}
/** UI-specific: Role badge variant */
get roleBadgeVariant(): string {
switch (this.role) {
case 'owner': return 'primary';
case 'captain': return 'secondary';
case 'member': return 'default';
default: return 'default';
}
}
/** UI-specific: Whether this member is the owner */
get isOwner(): boolean {
return this.driverId === this.teamOwnerId;
}
/** UI-specific: Whether current user can manage this member */
get canManage(): boolean {
return this.currentUserId === this.teamOwnerId && this.driverId !== this.currentUserId;
}
/** UI-specific: Whether this is the current user */
get isCurrentUser(): boolean {
return this.driverId === this.currentUserId;
}
/** UI-specific: Formatted joined date */
get formattedJoinedAt(): string {
return new Date(this.joinedAt).toLocaleDateString();
}
}

View File

@@ -0,0 +1,41 @@
import { TeamSummaryDto } from '../dtos';
export class TeamSummaryViewModel implements TeamSummaryDto {
id: string;
name: string;
logoUrl?: string;
memberCount: number;
rating: number;
private maxMembers = 10; // Assuming max members
constructor(dto: TeamSummaryDto) {
Object.assign(this, dto);
}
/** UI-specific: Whether team is full */
get isFull(): boolean {
return this.memberCount >= this.maxMembers;
}
/** UI-specific: Rating display */
get ratingDisplay(): string {
return this.rating.toFixed(0);
}
/** UI-specific: Member count display */
get memberCountDisplay(): string {
return `${this.memberCount}/${this.maxMembers}`;
}
/** UI-specific: Status indicator */
get statusIndicator(): string {
if (this.isFull) return 'Full';
return 'Open';
}
/** UI-specific: Status color */
get statusColor(): string {
return this.isFull ? 'red' : 'green';
}
}

View File

@@ -0,0 +1,37 @@
import { DriverDto } from '../dtos';
export class UserProfileViewModel implements DriverDto {
id: string;
name: string;
avatarUrl?: string;
iracingId?: string;
rating?: number;
constructor(dto: DriverDto) {
Object.assign(this, dto);
}
/** UI-specific: Formatted rating */
get formattedRating(): string {
return this.rating ? this.rating.toFixed(0) : 'Unrated';
}
/** UI-specific: Whether has iRacing ID */
get hasIracingId(): boolean {
return !!this.iracingId;
}
/** UI-specific: Profile completeness percentage */
get profileCompleteness(): number {
let complete = 1; // id always there
if (this.avatarUrl) complete++;
if (this.iracingId) complete++;
if (this.rating) complete++;
return Math.round((complete / 4) * 100);
}
/** UI-specific: Avatar initials */
get avatarInitials(): string {
return this.name.split(' ').map(n => n[0]).join('').toUpperCase();
}
}

View File

@@ -0,0 +1,34 @@
import { WalletTransactionDto } from '../dtos';
export class WalletTransactionViewModel implements WalletTransactionDto {
id: string;
type: 'deposit' | 'withdrawal';
amount: number;
description?: string;
createdAt: string;
constructor(dto: WalletTransactionDto) {
Object.assign(this, dto);
}
/** UI-specific: Formatted amount with sign */
get formattedAmount(): string {
const sign = this.type === 'deposit' ? '+' : '-';
return `${sign}$${this.amount.toFixed(2)}`;
}
/** UI-specific: Amount color */
get amountColor(): string {
return this.type === 'deposit' ? 'green' : 'red';
}
/** UI-specific: Type display */
get typeDisplay(): string {
return this.type.charAt(0).toUpperCase() + this.type.slice(1);
}
/** UI-specific: Formatted created date */
get formattedCreatedAt(): string {
return new Date(this.createdAt).toLocaleString();
}
}

View File

@@ -0,0 +1,36 @@
import { WalletDto, WalletTransactionDto } from '../dtos';
import { WalletTransactionViewModel } from './WalletTransactionViewModel';
export class WalletViewModel implements WalletDto {
driverId: string;
balance: number;
currency: string;
transactions: WalletTransactionViewModel[];
constructor(dto: WalletDto & { transactions: WalletTransactionDto[] }) {
this.driverId = dto.driverId;
this.balance = dto.balance;
this.currency = dto.currency;
this.transactions = dto.transactions.map(t => new WalletTransactionViewModel(t));
}
/** UI-specific: Formatted balance */
get formattedBalance(): string {
return `${this.currency} ${this.balance.toFixed(2)}`;
}
/** UI-specific: Balance color */
get balanceColor(): string {
return this.balance >= 0 ? 'green' : 'red';
}
/** UI-specific: Recent transactions (last 5) */
get recentTransactions(): WalletTransactionViewModel[] {
return this.transactions.slice(0, 5);
}
/** UI-specific: Total transactions count */
get totalTransactions(): number {
return this.transactions.length;
}
}

View File

@@ -0,0 +1,41 @@
// Analytics ViewModels
export { AnalyticsDashboardViewModel } from './AnalyticsDashboardViewModel';
export { AnalyticsMetricsViewModel } from './AnalyticsMetricsViewModel';
// Auth ViewModels
export { SessionViewModel } from './SessionViewModel';
export { UserProfileViewModel } from './UserProfileViewModel';
// Driver ViewModels
export { DriverLeaderboardItemViewModel } from './DriverLeaderboardItemViewModel';
export { DriverLeaderboardViewModel } from './DriverLeaderboardViewModel';
export { DriverRegistrationStatusViewModel } from './DriverRegistrationStatusViewModel';
// League ViewModels
export { LeagueMemberViewModel } from './LeagueMemberViewModel';
export { LeagueStandingsViewModel } from './LeagueStandingsViewModel';
export { LeagueSummaryViewModel } from './LeagueSummaryViewModel';
export { StandingEntryViewModel } from './StandingEntryViewModel';
// Payments ViewModels
export { MembershipFeeViewModel } from './MembershipFeeViewModel';
export { PaymentViewModel } from './PaymentViewModel';
export { PrizeViewModel } from './PrizeViewModel';
export { WalletTransactionViewModel } from './WalletTransactionViewModel';
export { WalletViewModel } from './WalletViewModel';
// Race ViewModels
export { RaceDetailViewModel } from './RaceDetailViewModel';
export { RaceListItemViewModel } from './RaceListItemViewModel';
export { RaceResultViewModel } from './RaceResultViewModel';
export { RaceResultsDetailViewModel } from './RaceResultsDetailViewModel';
// Sponsor ViewModels
export { SponsorViewModel } from './SponsorViewModel';
export { SponsorshipDetailViewModel } from './SponsorshipDetailViewModel';
// Team ViewModels
export { TeamDetailsViewModel } from './TeamDetailsViewModel';
export { TeamJoinRequestViewModel } from './TeamJoinRequestViewModel';
export { TeamMemberViewModel } from './TeamMemberViewModel';
export { TeamSummaryViewModel } from './TeamSummaryViewModel';