fix data flow issues

This commit is contained in:
2025-12-19 21:58:03 +01:00
parent 94fc538f44
commit ec177a75ce
37 changed files with 1336 additions and 534 deletions

View File

@@ -0,0 +1,74 @@
/**
* League Stewarding View Model
* Represents all data needed for league stewarding across all races
*/
export class LeagueStewardingViewModel {
constructor(
public readonly racesWithData: RaceWithProtests[],
public readonly driverMap: Record<string, { id: string; name: string; avatarUrl?: string; iracingId?: string; rating?: number }>
) {}
/** UI-specific: Total pending protests count */
get totalPending(): number {
return this.racesWithData.reduce((sum, r) => sum + r.pendingProtests.length, 0);
}
/** UI-specific: Total resolved protests count */
get totalResolved(): number {
return this.racesWithData.reduce((sum, r) => sum + r.resolvedProtests.length, 0);
}
/** UI-specific: Total penalties count */
get totalPenalties(): number {
return this.racesWithData.reduce((sum, r) => sum + r.penalties.length, 0);
}
/** UI-specific: Filtered races for pending tab */
get pendingRaces(): RaceWithProtests[] {
return this.racesWithData.filter(r => r.pendingProtests.length > 0);
}
/** UI-specific: Filtered races for history tab */
get historyRaces(): RaceWithProtests[] {
return this.racesWithData.filter(r => r.resolvedProtests.length > 0 || r.penalties.length > 0);
}
/** UI-specific: All drivers for quick penalty modal */
get allDrivers(): Array<{ id: string; name: string; avatarUrl?: string; iracingId?: string; rating?: number }> {
return Object.values(this.driverMap);
}
}
export interface RaceWithProtests {
race: {
id: string;
track: string;
scheduledAt: Date;
};
pendingProtests: Protest[];
resolvedProtests: Protest[];
penalties: Penalty[];
}
export interface Protest {
id: string;
protestingDriverId: string;
accusedDriverId: string;
incident: {
lap: number;
description: string;
};
filedAt: string;
status: string;
decisionNotes?: string;
proofVideoUrl?: string;
}
export interface Penalty {
id: string;
driverId: string;
type: string;
value: number;
reason: string;
notes?: string;
}

View File

@@ -0,0 +1,60 @@
import { WalletTransactionViewModel } from './WalletTransactionViewModel';
export class LeagueWalletViewModel {
balance: number;
currency: string;
totalRevenue: number;
totalFees: number;
totalWithdrawals: number;
pendingPayouts: number;
transactions: WalletTransactionViewModel[];
canWithdraw: boolean;
withdrawalBlockReason?: string;
constructor(dto: {
balance: number;
currency: string;
totalRevenue: number;
totalFees: number;
totalWithdrawals: number;
pendingPayouts: number;
transactions: WalletTransactionViewModel[];
canWithdraw: boolean;
withdrawalBlockReason?: string;
}) {
this.balance = dto.balance;
this.currency = dto.currency;
this.totalRevenue = dto.totalRevenue;
this.totalFees = dto.totalFees;
this.totalWithdrawals = dto.totalWithdrawals;
this.pendingPayouts = dto.pendingPayouts;
this.transactions = dto.transactions;
this.canWithdraw = dto.canWithdraw;
this.withdrawalBlockReason = dto.withdrawalBlockReason;
}
/** UI-specific: Formatted balance */
get formattedBalance(): string {
return `$${this.balance.toFixed(2)}`;
}
/** UI-specific: Formatted total revenue */
get formattedTotalRevenue(): string {
return `$${this.totalRevenue.toFixed(2)}`;
}
/** UI-specific: Formatted total fees */
get formattedTotalFees(): string {
return `$${this.totalFees.toFixed(2)}`;
}
/** UI-specific: Formatted pending payouts */
get formattedPendingPayouts(): string {
return `$${this.pendingPayouts.toFixed(2)}`;
}
/** UI-specific: Filtered transactions by type */
getFilteredTransactions(type: 'all' | 'sponsorship' | 'membership' | 'withdrawal' | 'prize'): WalletTransactionViewModel[] {
return type === 'all' ? this.transactions : this.transactions.filter(t => t.type === type);
}
}

View File

@@ -1,62 +1,65 @@
// Note: No generated DTO available for RaceListItem yet
// DTO matching the backend RacesPageDataRaceDTO
interface RaceListItemDTO {
id: string;
name: string;
track: string;
car: string;
scheduledAt: string;
status: string;
leagueId: string;
leagueName: string;
scheduledTime: string;
status: string;
trackName?: string;
strengthOfField: number | null;
isUpcoming: boolean;
isLive: boolean;
isPast: boolean;
}
export class RaceListItemViewModel {
id: string;
name: string;
track: string;
car: string;
scheduledAt: string;
status: string;
leagueId: string;
leagueName: string;
scheduledTime: string;
status: string;
trackName?: string;
strengthOfField: number | null;
isUpcoming: boolean;
isLive: boolean;
isPast: boolean;
constructor(dto: RaceListItemDTO) {
this.id = dto.id;
this.name = dto.name;
this.track = dto.track;
this.car = dto.car;
this.scheduledAt = dto.scheduledAt;
this.status = dto.status;
this.leagueId = dto.leagueId;
this.leagueName = dto.leagueName;
this.scheduledTime = dto.scheduledTime;
this.status = dto.status;
if (dto.trackName !== undefined) this.trackName = dto.trackName;
this.strengthOfField = dto.strengthOfField;
this.isUpcoming = dto.isUpcoming;
this.isLive = dto.isLive;
this.isPast = dto.isPast;
}
/** UI-specific: Formatted scheduled time */
get formattedScheduledTime(): string {
return new Date(this.scheduledTime).toLocaleString();
return new Date(this.scheduledAt).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';
case 'scheduled': return 'info';
case 'running': return 'success';
case 'completed': return 'secondary';
case 'cancelled': return 'danger';
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);
const scheduled = new Date(this.scheduledAt);
return Math.max(0, Math.floor((scheduled.getTime() - now.getTime()) / (1000 * 60)));
}

View File

@@ -0,0 +1,99 @@
// DTO interfaces matching the API responses
interface RaceDetailDTO {
race: {
id: string;
track: string;
scheduledAt: string;
status: string;
} | null;
league: {
id: string;
name: string;
} | null;
}
interface RaceProtestsDTO {
protests: Array<{
id: string;
protestingDriverId: string;
accusedDriverId: string;
incident: {
lap: number;
description: string;
};
filedAt: string;
status: string;
decisionNotes?: string;
proofVideoUrl?: string;
}>;
driverMap: Record<string, { id: string; name: string }>;
}
interface RacePenaltiesDTO {
penalties: Array<{
id: string;
driverId: string;
type: string;
value: number;
reason: string;
notes?: string;
}>;
driverMap: Record<string, { id: string; name: string }>;
}
interface RaceStewardingDTO {
raceDetail: RaceDetailDTO;
protests: RaceProtestsDTO;
penalties: RacePenaltiesDTO;
}
/**
* Race Stewarding View Model
* Represents all data needed for race stewarding (protests, penalties, race info)
*/
export class RaceStewardingViewModel {
race: RaceDetailDTO['race'];
league: RaceDetailDTO['league'];
protests: RaceProtestsDTO['protests'];
penalties: RacePenaltiesDTO['penalties'];
driverMap: Record<string, { id: string; name: string }>;
constructor(dto: RaceStewardingDTO) {
this.race = dto.raceDetail.race;
this.league = dto.raceDetail.league;
this.protests = dto.protests.protests;
this.penalties = dto.penalties.penalties;
// Merge driver maps from protests and penalties
this.driverMap = { ...dto.protests.driverMap, ...dto.penalties.driverMap };
}
/** UI-specific: Pending protests */
get pendingProtests() {
return this.protests.filter(p => p.status === 'pending' || p.status === 'under_review');
}
/** UI-specific: Resolved protests */
get resolvedProtests() {
return this.protests.filter(p =>
p.status === 'upheld' ||
p.status === 'dismissed' ||
p.status === 'withdrawn'
);
}
/** UI-specific: Total pending protests count */
get pendingCount(): number {
return this.pendingProtests.length;
}
/** UI-specific: Total resolved protests count */
get resolvedCount(): number {
return this.resolvedProtests.length;
}
/** UI-specific: Total penalties count */
get penaltiesCount(): number {
return this.penalties.length;
}
}

View File

@@ -1,63 +1,65 @@
// Note: No generated DTO available for RaceCard yet
interface RaceCardDTO {
id: string;
title: string;
scheduledTime: string;
status: string;
}
import { RaceListItemViewModel } from './RaceListItemViewModel';
/**
* Race card view model
* Represents a race card in list views
*/
export class RaceCardViewModel {
id: string;
title: string;
scheduledTime: string;
status: string;
constructor(dto: RaceCardDTO) {
this.id = dto.id;
this.title = dto.title;
this.scheduledTime = dto.scheduledTime;
this.status = dto.status;
}
/** UI-specific: Formatted scheduled time */
get formattedScheduledTime(): string {
return new Date(this.scheduledTime).toLocaleString();
}
}
// Note: No generated DTO available for RacesPage yet
// DTO matching the backend RacesPageDataDTO
interface RacesPageDTO {
upcomingRaces: RaceCardDTO[];
completedRaces: RaceCardDTO[];
totalCount: number;
races: Array<{
id: string;
track: string;
car: string;
scheduledAt: string;
status: string;
leagueId: string;
leagueName: string;
strengthOfField: number | null;
isUpcoming: boolean;
isLive: boolean;
isPast: boolean;
}>;
}
/**
* Races page view model
* Represents the races page data
* Represents the races page data with all races in a single list
*/
export class RacesPageViewModel {
upcomingRaces: RaceCardViewModel[];
completedRaces: RaceCardViewModel[];
totalCount: number;
races: RaceListItemViewModel[];
constructor(dto: RacesPageDTO) {
this.upcomingRaces = dto.upcomingRaces.map(r => new RaceCardViewModel(r));
this.completedRaces = dto.completedRaces.map(r => new RaceCardViewModel(r));
this.totalCount = dto.totalCount;
this.races = dto.races.map(r => new RaceListItemViewModel(r));
}
/** UI-specific: Total upcoming races */
get upcomingCount(): number {
return this.upcomingRaces.length;
/** UI-specific: Total races */
get totalCount(): number {
return this.races.length;
}
/** UI-specific: Total completed races */
get completedCount(): number {
return this.completedRaces.length;
/** UI-specific: Upcoming races */
get upcomingRaces(): RaceListItemViewModel[] {
return this.races.filter(r => r.isUpcoming);
}
/** UI-specific: Live races */
get liveRaces(): RaceListItemViewModel[] {
return this.races.filter(r => r.isLive);
}
/** UI-specific: Past races */
get pastRaces(): RaceListItemViewModel[] {
return this.races.filter(r => r.isPast);
}
/** UI-specific: Scheduled races */
get scheduledRaces(): RaceListItemViewModel[] {
return this.races.filter(r => r.status === 'scheduled');
}
/** UI-specific: Running races */
get runningRaces(): RaceListItemViewModel[] {
return this.races.filter(r => r.status === 'running');
}
/** UI-specific: Completed races */
get completedRaces(): RaceListItemViewModel[] {
return this.races.filter(r => r.status === 'completed');
}
}

View File

@@ -1,39 +1,45 @@
import { TransactionDto } from '../types/generated/TransactionDto';
// TODO: Use generated TransactionDto when it includes all required fields
export type FullTransactionDto = TransactionDto & {
amount: number;
description: string;
createdAt: string;
type: 'deposit' | 'withdrawal';
};
export class WalletTransactionViewModel {
id: string;
walletId: string;
amount: number;
type: 'sponsorship' | 'membership' | 'withdrawal' | 'prize';
description: string;
createdAt: string;
type: 'deposit' | 'withdrawal';
amount: number;
fee: number;
netAmount: number;
date: Date;
status: 'completed' | 'pending' | 'failed';
reference?: string;
constructor(dto: FullTransactionDto) {
constructor(dto: {
id: string;
type: 'sponsorship' | 'membership' | 'withdrawal' | 'prize';
description: string;
amount: number;
fee: number;
netAmount: number;
date: Date;
status: 'completed' | 'pending' | 'failed';
reference?: string;
}) {
this.id = dto.id;
this.walletId = dto.walletId;
this.amount = dto.amount;
this.description = dto.description;
this.createdAt = dto.createdAt;
this.type = dto.type;
this.description = dto.description;
this.amount = dto.amount;
this.fee = dto.fee;
this.netAmount = dto.netAmount;
this.date = dto.date;
this.status = dto.status;
this.reference = dto.reference;
}
/** UI-specific: Formatted amount with sign */
get formattedAmount(): string {
const sign = this.type === 'deposit' ? '+' : '-';
return `${sign}$${this.amount.toFixed(2)}`;
const sign = this.amount > 0 ? '+' : '';
return `${sign}$${Math.abs(this.amount).toFixed(2)}`;
}
/** UI-specific: Amount color */
get amountColor(): string {
return this.type === 'deposit' ? 'green' : 'red';
return this.amount > 0 ? 'green' : 'red';
}
/** UI-specific: Type display */
@@ -41,13 +47,13 @@ export class WalletTransactionViewModel {
return this.type.charAt(0).toUpperCase() + this.type.slice(1);
}
/** UI-specific: Amount color */
get amountColor(): string {
return this.type === 'deposit' ? 'green' : 'red';
/** UI-specific: Formatted date */
get formattedDate(): string {
return this.date.toLocaleDateString();
}
/** UI-specific: Formatted created date */
get formattedCreatedAt(): string {
return new Date(this.createdAt).toLocaleString();
/** UI-specific: Is incoming */
get isIncoming(): boolean {
return this.amount > 0;
}
}