This commit is contained in:
2025-12-10 18:28:32 +01:00
parent 6d61be9c51
commit 1303a14493
108 changed files with 3366 additions and 1559 deletions

View File

@@ -4,7 +4,8 @@ import {
type LeagueScheduleDTO,
type LeagueSchedulePreviewDTO,
} from '@gridpilot/racing/application';
import { getPreviewLeagueScheduleQuery } from '@/lib/di-container';
import { getPreviewLeagueScheduleUseCase } from '@/lib/di-container';
import { LeagueSchedulePreviewPresenter } from '@/lib/presenters/LeagueSchedulePreviewPresenter';
interface RequestBody {
seasonStartDate?: string;
@@ -73,11 +74,16 @@ export async function POST(request: NextRequest) {
const schedule = toLeagueScheduleDTO(json);
const query = getPreviewLeagueScheduleQuery();
const preview: LeagueSchedulePreviewDTO = await query.execute({
const presenter = new LeagueSchedulePreviewPresenter();
const useCase = getPreviewLeagueScheduleUseCase();
useCase.execute({
schedule,
maxRounds: 10,
});
const preview = presenter.getData();
if (!preview) {
return NextResponse.json({ error: 'Failed to generate preview' }, { status: 500 });
}
return NextResponse.json(preview, { status: 200 });
} catch (error) {

View File

@@ -1,7 +1,8 @@
import { NextResponse } from 'next/server';
import type { NextRequest } from 'next/server';
import { cookies } from 'next/headers';
import { getGetSponsorDashboardQuery } from '@/lib/di-container';
import { getGetSponsorDashboardUseCase } from '@/lib/di-container';
import { SponsorDashboardPresenter } from '@/lib/presenters/SponsorDashboardPresenter';
export async function GET(request: NextRequest) {
try {
@@ -16,8 +17,10 @@ export async function GET(request: NextRequest) {
);
}
const query = getGetSponsorDashboardQuery();
const dashboard = await query.execute({ sponsorId });
const presenter = new SponsorDashboardPresenter();
const useCase = getGetSponsorDashboardUseCase();
await useCase.execute({ sponsorId });
const dashboard = presenter.getData();
if (!dashboard) {
return NextResponse.json(

View File

@@ -1,7 +1,8 @@
import { NextResponse } from 'next/server';
import type { NextRequest } from 'next/server';
import { cookies } from 'next/headers';
import { getGetSponsorSponsorshipsQuery } from '@/lib/di-container';
import { getGetSponsorSponsorshipsUseCase } from '@/lib/di-container';
import { SponsorSponsorshipsPresenter } from '@/lib/presenters/SponsorSponsorshipsPresenter';
export async function GET(request: NextRequest) {
try {
@@ -16,8 +17,10 @@ export async function GET(request: NextRequest) {
);
}
const query = getGetSponsorSponsorshipsQuery();
const sponsorships = await query.execute({ sponsorId });
const presenter = new SponsorSponsorshipsPresenter();
const useCase = getGetSponsorSponsorshipsUseCase();
await useCase.execute({ sponsorId });
const sponsorships = presenter.getData();
if (!sponsorships) {
return NextResponse.json(

View File

@@ -40,11 +40,11 @@ import {
getDriverRepository,
getDriverStats,
getAllDriverRankings,
getGetDriverTeamQuery,
getGetDriverTeamUseCase,
getSocialRepository,
getImageService,
getGetAllTeamsQuery,
getGetTeamMembersQuery,
getGetAllTeamsUseCase,
getGetTeamMembersUseCase,
} from '@/lib/di-container';
import { Driver, EntityMappers, type Team } from '@gridpilot/racing';
import type { DriverDTO } from '@gridpilot/racing';
@@ -382,18 +382,23 @@ export default function DriverDetailPage({
setDriver(driverDto);
// Load team data
const teamQuery = getGetDriverTeamQuery();
const teamResult = await teamQuery.execute({ driverId });
setTeamData(teamResult);
const teamUseCase = getGetDriverTeamUseCase();
await teamUseCase.execute({ driverId });
const teamViewModel = teamUseCase.presenter.getViewModel();
setTeamData(teamViewModel.result);
// Load ALL team memberships
const allTeamsQuery = getGetAllTeamsQuery();
const allTeams = await allTeamsQuery.execute();
const membershipsQuery = getGetTeamMembersQuery();
const allTeamsUseCase = getGetAllTeamsUseCase();
await allTeamsUseCase.execute();
const allTeamsViewModel = allTeamsUseCase.presenter.getViewModel();
const allTeams = allTeamsViewModel.teams;
const membershipsUseCase = getGetTeamMembersUseCase();
const memberships: TeamMembershipInfo[] = [];
for (const team of allTeams) {
const members = await membershipsQuery.execute({ teamId: team.id });
await membershipsUseCase.execute({ teamId: team.id });
const membersViewModel = membershipsUseCase.presenter.getViewModel();
const members = membersViewModel.members;
const membership = members.find((m) => m.driverId === driverId);
if (membership) {
memberships.push({

View File

@@ -27,27 +27,15 @@ import Button from '@/components/ui/Button';
import Input from '@/components/ui/Input';
import Card from '@/components/ui/Card';
import Heading from '@/components/ui/Heading';
import { getDriverRepository, getDriverStats, getAllDriverRankings, getImageService } from '@/lib/di-container';
import { getGetDriversLeaderboardUseCase } from '@/lib/di-container';
import type { DriverLeaderboardItemViewModel, SkillLevel } from '@gridpilot/racing/application/presenters/IDriversLeaderboardPresenter';
import Image from 'next/image';
// ============================================================================
// TYPES
// ============================================================================
type SkillLevel = 'beginner' | 'intermediate' | 'advanced' | 'pro';
interface DriverListItem {
id: string;
name: string;
rating: number;
skillLevel: SkillLevel;
nationality: string;
racesCompleted: number;
wins: number;
podiums: number;
isActive: boolean;
rank: number;
}
type DriverListItem = DriverLeaderboardItemViewModel;
// ============================================================================
// DEMO DATA
@@ -87,7 +75,6 @@ interface FeaturedDriverCardProps {
}
function FeaturedDriverCard({ driver, position, onClick }: FeaturedDriverCardProps) {
const imageService = getImageService();
const levelConfig = SKILL_LEVELS.find((l) => l.id === driver.skillLevel);
const getBorderColor = (pos: number) => {
@@ -131,7 +118,7 @@ function FeaturedDriverCard({ driver, position, onClick }: FeaturedDriverCardPro
{/* Avatar & Name */}
<div className="flex items-center gap-4 mb-4">
<div className="relative w-16 h-16 rounded-full overflow-hidden border-2 border-charcoal-outline group-hover:border-primary-blue transition-colors">
<Image src={imageService.getDriverAvatar(driver.id)} alt={driver.name} fill className="object-cover" />
<Image src={driver.avatarUrl} alt={driver.name} fill className="object-cover" />
</div>
<div>
<h3 className="text-lg font-semibold text-white group-hover:text-primary-blue transition-colors">
@@ -236,7 +223,6 @@ interface LeaderboardPreviewProps {
function LeaderboardPreview({ drivers, onDriverClick }: LeaderboardPreviewProps) {
const router = useRouter();
const imageService = getImageService();
const top5 = drivers.slice(0, 5);
const getMedalColor = (position: number) => {
@@ -300,7 +286,7 @@ function LeaderboardPreview({ drivers, onDriverClick }: LeaderboardPreviewProps)
{/* Avatar */}
<div className="relative w-9 h-9 rounded-full overflow-hidden border-2 border-charcoal-outline">
<Image src={imageService.getDriverAvatar(driver.id)} alt={driver.name} fill className="object-cover" />
<Image src={driver.avatarUrl} alt={driver.name} fill className="object-cover" />
</div>
{/* Info */}
@@ -345,7 +331,6 @@ interface RecentActivityProps {
}
function RecentActivity({ drivers, onDriverClick }: RecentActivityProps) {
const imageService = getImageService();
const activeDrivers = drivers.filter((d) => d.isActive).slice(0, 6);
return (
@@ -371,7 +356,7 @@ function RecentActivity({ drivers, onDriverClick }: RecentActivityProps) {
className="p-3 rounded-xl bg-iron-gray/40 border border-charcoal-outline hover:border-performance-green/40 transition-all group text-center"
>
<div className="relative w-12 h-12 mx-auto rounded-full overflow-hidden border-2 border-charcoal-outline mb-2">
<Image src={imageService.getDriverAvatar(driver.id)} alt={driver.name} fill className="object-cover" />
<Image src={driver.avatarUrl} alt={driver.name} fill className="object-cover" />
<div className="absolute bottom-0 right-0 w-3 h-3 rounded-full bg-performance-green border-2 border-iron-gray" />
</div>
<p className="text-sm font-medium text-white truncate group-hover:text-performance-green transition-colors">
@@ -395,57 +380,20 @@ export default function DriversPage() {
const [drivers, setDrivers] = useState<DriverListItem[]>([]);
const [loading, setLoading] = useState(true);
const [searchQuery, setSearchQuery] = useState('');
const [totalRaces, setTotalRaces] = useState(0);
const [totalWins, setTotalWins] = useState(0);
const [activeCount, setActiveCount] = useState(0);
useEffect(() => {
const load = async () => {
const driverRepo = getDriverRepository();
const allDrivers = await driverRepo.findAll();
const rankings = getAllDriverRankings();
const useCase = getGetDriversLeaderboardUseCase();
await useCase.execute();
const viewModel = useCase.presenter.getViewModel();
const items: DriverListItem[] = allDrivers.map((driver) => {
const stats = getDriverStats(driver.id);
const rating = stats?.rating ?? 0;
const wins = stats?.wins ?? 0;
const podiums = stats?.podiums ?? 0;
const totalRaces = stats?.totalRaces ?? 0;
let effectiveRank = Number.POSITIVE_INFINITY;
if (typeof stats?.overallRank === 'number' && stats.overallRank > 0) {
effectiveRank = stats.overallRank;
} else {
const indexInGlobal = rankings.findIndex((entry) => entry.driverId === driver.id);
if (indexInGlobal !== -1) {
effectiveRank = indexInGlobal + 1;
}
}
const skillLevel: SkillLevel =
rating >= 3000 ? 'pro' : rating >= 2500 ? 'advanced' : rating >= 1800 ? 'intermediate' : 'beginner';
const isActive = rankings.some((r) => r.driverId === driver.id);
return {
id: driver.id,
name: driver.name,
rating,
skillLevel,
nationality: driver.country,
racesCompleted: totalRaces,
wins,
podiums,
isActive,
rank: effectiveRank,
};
});
// Sort by rank
items.sort((a, b) => {
const rankA = Number.isFinite(a.rank) && a.rank > 0 ? a.rank : Number.POSITIVE_INFINITY;
const rankB = Number.isFinite(b.rank) && b.rank > 0 ? b.rank : Number.POSITIVE_INFINITY;
return rankA - rankB || b.rating - a.rating;
});
setDrivers(items);
setDrivers(viewModel.drivers);
setTotalRaces(viewModel.totalRaces);
setTotalWins(viewModel.totalWins);
setActiveCount(viewModel.activeCount);
setLoading(false);
};
@@ -465,10 +413,6 @@ export default function DriversPage() {
);
});
// Stats
const totalRaces = drivers.reduce((sum, d) => sum + d.racesCompleted, 0);
const totalWins = drivers.reduce((sum, d) => sum + d.wins, 0);
const activeCount = drivers.filter((d) => d.isActive).length;
// Featured drivers (top 4)
const featuredDrivers = filteredDrivers.slice(0, 4);

View File

@@ -19,27 +19,17 @@ import {
import Button from '@/components/ui/Button';
import Input from '@/components/ui/Input';
import Heading from '@/components/ui/Heading';
import { getDriverRepository, getDriverStats, getAllDriverRankings, getImageService } from '@/lib/di-container';
import { getGetDriversLeaderboardUseCase } from '@/lib/di-container';
import type { DriverLeaderboardItemViewModel, SkillLevel } from '@gridpilot/racing/application/presenters/IDriversLeaderboardPresenter';
import Image from 'next/image';
// ============================================================================
// TYPES
// ============================================================================
type SkillLevel = 'beginner' | 'intermediate' | 'advanced' | 'pro';
type SortBy = 'rank' | 'rating' | 'wins' | 'podiums' | 'winRate';
interface DriverListItem {
id: string;
name: string;
rating: number;
skillLevel: SkillLevel;
nationality: string;
racesCompleted: number;
wins: number;
podiums: number;
rank: number;
}
type DriverListItem = DriverLeaderboardItemViewModel;
// ============================================================================
// SKILL LEVEL CONFIG
@@ -81,7 +71,6 @@ interface TopThreePodiumProps {
}
function TopThreePodium({ drivers, onDriverClick }: TopThreePodiumProps) {
const imageService = getImageService();
const top3 = drivers.slice(0, 3);
if (top3.length < 3) return null;
@@ -122,7 +111,7 @@ function TopThreePodium({ drivers, onDriverClick }: TopThreePodiumProps) {
{/* Avatar */}
<div className={`relative ${position === 1 ? 'w-24 h-24 lg:w-28 lg:h-28' : 'w-20 h-20 lg:w-24 lg:h-24'} rounded-full overflow-hidden border-4 ${position === 1 ? 'border-yellow-400 shadow-[0_0_30px_rgba(250,204,21,0.3)]' : position === 2 ? 'border-gray-300' : 'border-amber-600'} group-hover:scale-105 transition-transform`}>
<Image
src={imageService.getDriverAvatar(driver.id)}
src={driver.avatarUrl}
alt={driver.name}
fill
className="object-cover"
@@ -178,7 +167,6 @@ function TopThreePodium({ drivers, onDriverClick }: TopThreePodiumProps) {
export default function DriverLeaderboardPage() {
const router = useRouter();
const imageService = getImageService();
const [drivers, setDrivers] = useState<DriverListItem[]>([]);
const [loading, setLoading] = useState(true);
const [searchQuery, setSearchQuery] = useState('');
@@ -188,44 +176,10 @@ export default function DriverLeaderboardPage() {
useEffect(() => {
const load = async () => {
const driverRepo = getDriverRepository();
const allDrivers = await driverRepo.findAll();
const rankings = getAllDriverRankings();
const items: DriverListItem[] = allDrivers.map((driver) => {
const stats = getDriverStats(driver.id);
const rating = stats?.rating ?? 0;
const wins = stats?.wins ?? 0;
const podiums = stats?.podiums ?? 0;
const totalRaces = stats?.totalRaces ?? 0;
let effectiveRank = Number.POSITIVE_INFINITY;
if (typeof stats?.overallRank === 'number' && stats.overallRank > 0) {
effectiveRank = stats.overallRank;
} else {
const indexInGlobal = rankings.findIndex((entry) => entry.driverId === driver.id);
if (indexInGlobal !== -1) {
effectiveRank = indexInGlobal + 1;
}
}
const skillLevel: SkillLevel =
rating >= 3000 ? 'pro' : rating >= 2500 ? 'advanced' : rating >= 1800 ? 'intermediate' : 'beginner';
return {
id: driver.id,
name: driver.name,
rating,
skillLevel,
nationality: driver.country,
racesCompleted: totalRaces,
wins,
podiums,
rank: effectiveRank,
};
});
setDrivers(items);
const useCase = getGetDriversLeaderboardUseCase();
await useCase.execute();
const viewModel = useCase.presenter.getViewModel();
setDrivers(viewModel.drivers);
setLoading(false);
};
@@ -443,7 +397,7 @@ export default function DriverLeaderboardPage() {
{/* Driver Info */}
<div className="col-span-5 lg:col-span-4 flex items-center gap-3">
<div className="relative w-10 h-10 rounded-full overflow-hidden border-2 border-charcoal-outline">
<Image src={imageService.getDriverAvatar(driver.id)} alt={driver.name} fill className="object-cover" />
<Image src={driver.avatarUrl} alt={driver.name} fill className="object-cover" />
</div>
<div className="min-w-0">
<p className="text-white font-semibold truncate group-hover:text-primary-blue transition-colors">

View File

@@ -20,36 +20,18 @@ import {
} from 'lucide-react';
import Button from '@/components/ui/Button';
import Heading from '@/components/ui/Heading';
import { getDriverRepository, getDriverStats, getAllDriverRankings, getImageService, getGetAllTeamsQuery, getGetTeamMembersQuery } from '@/lib/di-container';
import { getGetDriversLeaderboardUseCase, getGetTeamsLeaderboardUseCase } from '@/lib/di-container';
import type { DriverLeaderboardItemViewModel, SkillLevel } from '@gridpilot/racing/application/presenters/IDriversLeaderboardPresenter';
import type { TeamLeaderboardItemViewModel } from '@gridpilot/racing/application/presenters/ITeamsLeaderboardPresenter';
import Image from 'next/image';
import type { Team } from '@gridpilot/racing';
// ============================================================================
// TYPES
// ============================================================================
type SkillLevel = 'beginner' | 'intermediate' | 'advanced' | 'pro';
type DriverListItem = DriverLeaderboardItemViewModel;
interface DriverListItem {
id: string;
name: string;
rating: number;
skillLevel: SkillLevel;
nationality: string;
wins: number;
podiums: number;
rank: number;
}
interface TeamDisplayData {
id: string;
name: string;
memberCount: number;
rating: number | null;
totalWins: number;
totalRaces: number;
performanceLevel: SkillLevel;
}
type TeamDisplayData = TeamLeaderboardItemViewModel;
// ============================================================================
// SKILL LEVEL CONFIG
@@ -80,7 +62,6 @@ interface DriverLeaderboardPreviewProps {
function DriverLeaderboardPreview({ drivers, onDriverClick }: DriverLeaderboardPreviewProps) {
const router = useRouter();
const imageService = getImageService();
const top10 = drivers.slice(0, 10);
const getMedalColor = (position: number) => {
@@ -144,7 +125,7 @@ function DriverLeaderboardPreview({ drivers, onDriverClick }: DriverLeaderboardP
{/* Avatar */}
<div className="relative w-9 h-9 rounded-full overflow-hidden border-2 border-charcoal-outline">
<Image src={imageService.getDriverAvatar(driver.id)} alt={driver.name} fill className="object-cover" />
<Image src={driver.avatarUrl} alt={driver.name} fill className="object-cover" />
</div>
{/* Info */}
@@ -189,7 +170,6 @@ interface TeamLeaderboardPreviewProps {
function TeamLeaderboardPreview({ teams, onTeamClick }: TeamLeaderboardPreviewProps) {
const router = useRouter();
const imageService = getImageService();
const top5 = [...teams]
.filter((t) => t.rating !== null)
.sort((a, b) => (b.rating ?? 0) - (a.rating ?? 0))
@@ -304,99 +284,16 @@ export default function LeaderboardsPage() {
useEffect(() => {
const load = async () => {
try {
// Load drivers
const driverRepo = getDriverRepository();
const allDrivers = await driverRepo.findAll();
const rankings = getAllDriverRankings();
const driversUseCase = getGetDriversLeaderboardUseCase();
const teamsUseCase = getGetTeamsLeaderboardUseCase();
await driversUseCase.execute();
await teamsUseCase.execute();
const driverItems: DriverListItem[] = allDrivers.map((driver) => {
const stats = getDriverStats(driver.id);
const rating = stats?.rating ?? 0;
const wins = stats?.wins ?? 0;
const podiums = stats?.podiums ?? 0;
const driversViewModel = driversUseCase.presenter.getViewModel();
const teamsViewModel = teamsUseCase.presenter.getViewModel();
let effectiveRank = Number.POSITIVE_INFINITY;
if (typeof stats?.overallRank === 'number' && stats.overallRank > 0) {
effectiveRank = stats.overallRank;
} else {
const indexInGlobal = rankings.findIndex((entry) => entry.driverId === driver.id);
if (indexInGlobal !== -1) {
effectiveRank = indexInGlobal + 1;
}
}
const skillLevel: SkillLevel =
rating >= 3000 ? 'pro' : rating >= 2500 ? 'advanced' : rating >= 1800 ? 'intermediate' : 'beginner';
return {
id: driver.id,
name: driver.name,
rating,
skillLevel,
nationality: driver.country,
wins,
podiums,
rank: effectiveRank,
};
});
// Sort by rank
driverItems.sort((a, b) => {
const rankA = Number.isFinite(a.rank) && a.rank > 0 ? a.rank : Number.POSITIVE_INFINITY;
const rankB = Number.isFinite(b.rank) && b.rank > 0 ? b.rank : Number.POSITIVE_INFINITY;
return rankA - rankB || b.rating - a.rating;
});
// Load teams
const allTeamsQuery = getGetAllTeamsQuery();
const teamMembersQuery = getGetTeamMembersQuery();
const allTeams = await allTeamsQuery.execute();
const teamData: TeamDisplayData[] = [];
await Promise.all(
allTeams.map(async (team: Team) => {
const memberships = await teamMembersQuery.execute({ teamId: team.id });
const memberCount = memberships.length;
let ratingSum = 0;
let ratingCount = 0;
let totalWins = 0;
let totalRaces = 0;
for (const membership of memberships) {
const stats = getDriverStats(membership.driverId);
if (!stats) continue;
if (typeof stats.rating === 'number') {
ratingSum += stats.rating;
ratingCount += 1;
}
totalWins += stats.wins ?? 0;
totalRaces += stats.totalRaces ?? 0;
}
const averageRating = ratingCount > 0 ? ratingSum / ratingCount : null;
let performanceLevel: SkillLevel = 'beginner';
if (averageRating !== null) {
if (averageRating >= 4500) performanceLevel = 'pro';
else if (averageRating >= 3000) performanceLevel = 'advanced';
else if (averageRating >= 2000) performanceLevel = 'intermediate';
}
teamData.push({
id: team.id,
name: team.name,
memberCount,
rating: averageRating,
totalWins,
totalRaces,
performanceLevel,
});
}),
);
setDrivers(driverItems);
setTeams(teamData);
setDrivers(driversViewModel.drivers);
setTeams(teamsViewModel.teams);
} catch (error) {
console.error('Failed to load leaderboard data:', error);
setDrivers([]);

View File

@@ -25,10 +25,10 @@ import {
getLeagueRepository,
getRaceRepository,
getDriverRepository,
getGetLeagueScoringConfigQuery,
getGetLeagueScoringConfigUseCase,
getDriverStats,
getAllDriverRankings,
getGetLeagueStatsQuery,
getGetLeagueStatsUseCase,
getSeasonRepository,
getSponsorRepository,
getSeasonSponsorshipRepository,
@@ -104,7 +104,7 @@ export default function LeagueDetailPage() {
const leagueRepo = getLeagueRepository();
const raceRepo = getRaceRepository();
const driverRepo = getDriverRepository();
const leagueStatsQuery = getGetLeagueStatsQuery();
const leagueStatsUseCase = getGetLeagueStatsUseCase();
const seasonRepo = getSeasonRepository();
const sponsorRepo = getSponsorRepository();
const sponsorshipRepo = getSeasonSponsorshipRepository();
@@ -124,9 +124,10 @@ export default function LeagueDetailPage() {
setOwner(ownerData);
// Load scoring configuration for the active season
const getLeagueScoringConfigQuery = getGetLeagueScoringConfigQuery();
const scoring = await getLeagueScoringConfigQuery.execute({ leagueId });
setScoringConfig(scoring);
const getLeagueScoringConfigUseCase = getGetLeagueScoringConfigUseCase();
await getLeagueScoringConfigUseCase.execute({ leagueId });
const scoringViewModel = getLeagueScoringConfigUseCase.presenter.getViewModel();
setScoringConfig(scoringViewModel);
// Load all drivers for standings and map to DTOs for UI components
const allDrivers = await driverRepo.findAll();
@@ -136,11 +137,12 @@ export default function LeagueDetailPage() {
setDrivers(driverDtos);
// Load league stats including average SOF from application query
const leagueStats = await leagueStatsQuery.execute({ leagueId });
if (leagueStats) {
setAverageSOF(leagueStats.averageSOF);
setCompletedRacesCount(leagueStats.completedRaces);
// Load league stats including average SOF from application use case
await leagueStatsUseCase.execute({ leagueId });
const leagueStatsViewModel = leagueStatsUseCase.presenter.getViewModel();
if (leagueStatsViewModel) {
setAverageSOF(leagueStatsViewModel.averageSOF);
setCompletedRacesCount(leagueStatsViewModel.completedRaces);
} else {
// Fallback: count completed races manually
const leagueRaces = await raceRepo.findByLeagueId(leagueId);

View File

@@ -5,7 +5,7 @@ import { useParams } from 'next/navigation';
import Card from '@/components/ui/Card';
import {
getLeagueRepository,
getGetLeagueScoringConfigQuery
getGetLeagueScoringConfigUseCase
} from '@/lib/di-container';
import type { LeagueScoringConfigDTO } from '@gridpilot/racing/application/dto/LeagueScoringConfigDTO';
import type { League } from '@gridpilot/racing/domain/entities/League';
@@ -25,7 +25,7 @@ export default function LeagueRulebookPage() {
async function loadData() {
try {
const leagueRepo = getLeagueRepository();
const scoringQuery = getGetLeagueScoringConfigQuery();
const scoringUseCase = getGetLeagueScoringConfigUseCase();
const leagueData = await leagueRepo.findById(leagueId);
if (!leagueData) {
@@ -35,8 +35,9 @@ export default function LeagueRulebookPage() {
setLeague(leagueData);
const scoring = await scoringQuery.execute({ leagueId });
setScoringConfig(scoring);
await scoringUseCase.execute({ leagueId });
const scoringViewModel = scoringUseCase.presenter.getViewModel();
setScoringConfig(scoringViewModel);
} catch (err) {
console.error('Failed to load scoring config:', err);
} finally {

View File

@@ -7,11 +7,11 @@ import Button from '@/components/ui/Button';
import {
getLeagueRepository,
getDriverRepository,
getGetLeagueFullConfigQuery,
getGetLeagueFullConfigUseCase,
getLeagueMembershipRepository,
getDriverStats,
getAllDriverRankings,
getListLeagueScoringPresetsQuery,
getListLeagueScoringPresetsUseCase,
getTransferLeagueOwnershipUseCase
} from '@/lib/di-container';
import { useEffectiveDriverId } from '@/lib/currentDriver';
@@ -59,8 +59,8 @@ export default function LeagueSettingsPage() {
try {
const leagueRepo = getLeagueRepository();
const driverRepo = getDriverRepository();
const query = getGetLeagueFullConfigQuery();
const presetsQuery = getListLeagueScoringPresetsQuery();
const useCase = getGetLeagueFullConfigUseCase();
const presetsUseCase = getListLeagueScoringPresetsUseCase();
const leagueData = await leagueRepo.findById(leagueId);
if (!leagueData) {
@@ -70,11 +70,13 @@ export default function LeagueSettingsPage() {
setLeague(leagueData);
const form = await query.execute({ leagueId });
setConfigForm(form);
await useCase.execute({ leagueId });
const configViewModel = useCase.presenter.getViewModel();
setConfigForm(configViewModel);
const presetsData = await presetsQuery.execute();
setPresets(presetsData);
await presetsUseCase.execute();
const presetsViewModel = presetsUseCase.presenter.getViewModel();
setPresets(presetsViewModel);
const entity = await driverRepo.findById(leagueData.ownerId);
if (entity) {

View File

@@ -10,7 +10,7 @@ import {
type LeagueDriverSeasonStatsDTO,
} from '@gridpilot/racing';
import {
getGetLeagueDriverSeasonStatsQuery,
getGetLeagueDriverSeasonStatsUseCase,
getDriverRepository,
getLeagueMembershipRepository
} from '@/lib/di-container';
@@ -32,12 +32,13 @@ export default function LeagueStandingsPage() {
const loadData = useCallback(async () => {
try {
const getLeagueDriverSeasonStatsQuery = getGetLeagueDriverSeasonStatsQuery();
const getLeagueDriverSeasonStatsUseCase = getGetLeagueDriverSeasonStatsUseCase();
const driverRepo = getDriverRepository();
const membershipRepo = getLeagueMembershipRepository();
const leagueStandings = await getLeagueDriverSeasonStatsQuery.execute({ leagueId });
setStandings(leagueStandings);
await getLeagueDriverSeasonStatsUseCase.execute({ leagueId });
const standingsViewModel = getLeagueDriverSeasonStatsUseCase.presenter.getViewModel();
setStandings(standingsViewModel);
const allDrivers = await driverRepo.findAll();
const driverDtos: DriverDTO[] = allDrivers

View File

@@ -31,7 +31,7 @@ import Card from '@/components/ui/Card';
import Input from '@/components/ui/Input';
import Heading from '@/components/ui/Heading';
import type { LeagueSummaryDTO } from '@gridpilot/racing/application/dto/LeagueSummaryDTO';
import { getGetAllLeaguesWithCapacityAndScoringQuery } from '@/lib/di-container';
import { getGetAllLeaguesWithCapacityAndScoringUseCase } from '@/lib/di-container';
// ============================================================================
// TYPES
@@ -389,9 +389,10 @@ export default function LeaguesPage() {
const loadLeagues = async () => {
try {
const query = getGetAllLeaguesWithCapacityAndScoringQuery();
const allLeagues = await query.execute();
setRealLeagues(allLeagues);
const useCase = getGetAllLeaguesWithCapacityAndScoringUseCase();
await useCase.execute();
const viewModel = useCase.presenter.getViewModel();
setRealLeagues(viewModel);
} catch (error) {
console.error('Failed to load leagues:', error);
} finally {

View File

@@ -38,11 +38,11 @@ import {
getDriverRepository,
getDriverStats,
getAllDriverRankings,
getGetDriverTeamQuery,
getGetDriverTeamUseCase,
getSocialRepository,
getImageService,
getGetAllTeamsQuery,
getGetTeamMembersQuery,
getGetAllTeamsUseCase,
getGetTeamMembersUseCase,
} from '@/lib/di-container';
import { Driver, EntityMappers, type DriverDTO, type Team } from '@gridpilot/racing';
import CreateDriverForm from '@/components/drivers/CreateDriverForm';
@@ -381,18 +381,23 @@ export default function ProfilePage() {
setDriver(driverData);
// Load primary team data
const teamQuery = getGetDriverTeamQuery();
const teamResult = await teamQuery.execute({ driverId: currentDriverId });
setTeamData(teamResult);
const teamUseCase = getGetDriverTeamUseCase();
await teamUseCase.execute({ driverId: currentDriverId });
const teamViewModel = teamUseCase.presenter.getViewModel();
setTeamData(teamViewModel.result);
// Load ALL team memberships
const allTeamsQuery = getGetAllTeamsQuery();
const allTeams = await allTeamsQuery.execute();
const membershipsQuery = getGetTeamMembersQuery();
const allTeamsUseCase = getGetAllTeamsUseCase();
await allTeamsUseCase.execute();
const allTeamsViewModel = allTeamsUseCase.presenter.getViewModel();
const allTeams = allTeamsViewModel.teams;
const membershipsUseCase = getGetTeamMembersUseCase();
const memberships: TeamMembershipInfo[] = [];
for (const team of allTeams) {
const members = await membershipsQuery.execute({ teamId: team.id });
await membershipsUseCase.execute({ teamId: team.id });
const membersViewModel = membershipsUseCase.presenter.getViewModel();
const members = membersViewModel.members;
const membership = members.find((m) => m.driverId === currentDriverId);
if (membership) {
memberships.push({

View File

@@ -7,7 +7,7 @@ import Button from '@/components/ui/Button';
import Breadcrumbs from '@/components/layout/Breadcrumbs';
import PendingSponsorshipRequests, { type PendingRequestDTO } from '@/components/sponsors/PendingSponsorshipRequests';
import {
getGetPendingSponsorshipRequestsQuery,
getGetPendingSponsorshipRequestsUseCase,
getAcceptSponsorshipRequestUseCase,
getRejectSponsorshipRequestUseCase,
getDriverRepository,
@@ -46,7 +46,7 @@ export default function SponsorshipRequestsPage() {
const teamRepo = getTeamRepository();
const leagueMembershipRepo = getLeagueMembershipRepository();
const teamMembershipRepo = getTeamMembershipRepository();
const query = getGetPendingSponsorshipRequestsQuery();
const useCase = getGetPendingSponsorshipRequestsUseCase();
const allSections: EntitySection[] = [];

View File

@@ -17,11 +17,11 @@ import {
getRaceRepository,
getLeagueRepository,
getDriverRepository,
getGetRaceRegistrationsQuery,
getIsDriverRegisteredForRaceQuery,
getGetRaceRegistrationsUseCase,
getIsDriverRegisteredForRaceUseCase,
getRegisterForRaceUseCase,
getWithdrawFromRaceUseCase,
getGetRaceWithSOFQuery,
getGetRaceWithSOFUseCase,
getResultRepository,
getImageService,
} from '@/lib/di-container';
@@ -80,7 +80,7 @@ export default function RaceDetailPage() {
try {
const raceRepo = getRaceRepository();
const leagueRepo = getLeagueRepository();
const raceWithSOFQuery = getGetRaceWithSOFQuery();
const raceWithSOFUseCase = getGetRaceWithSOFUseCase();
const raceData = await raceRepo.findById(raceId);
@@ -92,10 +92,11 @@ export default function RaceDetailPage() {
setRace(raceData);
// Load race with SOF from application query
const raceWithSOF = await raceWithSOFQuery.execute({ raceId });
if (raceWithSOF) {
setRaceSOF(raceWithSOF.strengthOfField);
// Load race with SOF from application use case
await raceWithSOFUseCase.execute({ raceId });
const raceViewModel = raceWithSOFUseCase.presenter.getViewModel();
if (raceViewModel) {
setRaceSOF(raceViewModel.strengthOfField);
}
// Load league data
@@ -135,8 +136,10 @@ export default function RaceDetailPage() {
try {
const driverRepo = getDriverRepository();
const raceRegistrationsQuery = getGetRaceRegistrationsQuery();
const registeredDriverIds = await raceRegistrationsQuery.execute({ raceId });
const raceRegistrationsUseCase = getGetRaceRegistrationsUseCase();
await raceRegistrationsUseCase.execute({ raceId });
const registrationsViewModel = raceRegistrationsUseCase.presenter.getViewModel();
const registeredDriverIds = registrationsViewModel.registeredDriverIds;
const drivers = await Promise.all(
registeredDriverIds.map((id: string) => driverRepo.findById(id)),
@@ -144,12 +147,13 @@ export default function RaceDetailPage() {
const validDrivers = drivers.filter((d: Driver | null): d is Driver => d !== null);
setEntryList(validDrivers);
const isRegisteredQuery = getIsDriverRegisteredForRaceQuery();
const userIsRegistered = await isRegisteredQuery.execute({
const isRegisteredUseCase = getIsDriverRegisteredForRaceUseCase();
await isRegisteredUseCase.execute({
raceId,
driverId: currentDriverId,
});
setIsUserRegistered(userIsRegistered);
const registrationViewModel = isRegisteredUseCase.presenter.getViewModel();
setIsUserRegistered(registrationViewModel.isRegistered);
const membership = getMembership(leagueId, currentDriverId);
const isUpcoming = race?.status === 'scheduled';

View File

@@ -18,8 +18,8 @@ import {
getResultRepository,
getStandingRepository,
getDriverRepository,
getGetRaceWithSOFQuery,
getGetRacePenaltiesQuery,
getGetRaceWithSOFUseCase,
getGetRacePenaltiesUseCase,
} from '@/lib/di-container';
interface PenaltyData {
@@ -52,7 +52,7 @@ export default function RaceResultsPage() {
const leagueRepo = getLeagueRepository();
const resultRepo = getResultRepository();
const driverRepo = getDriverRepository();
const raceWithSOFQuery = getGetRaceWithSOFQuery();
const raceWithSOFUseCase = getGetRaceWithSOFUseCase();
const raceData = await raceRepo.findById(raceId);
@@ -64,10 +64,11 @@ export default function RaceResultsPage() {
setRace(raceData);
// Load race with SOF from application query
const raceWithSOF = await raceWithSOFQuery.execute({ raceId });
if (raceWithSOF) {
setRaceSOF(raceWithSOF.strengthOfField);
// Load race with SOF from application use case
await raceWithSOFUseCase.execute({ raceId });
const raceViewModel = raceWithSOFUseCase.presenter.getViewModel();
if (raceViewModel) {
setRaceSOF(raceViewModel.strengthOfField);
}
// Load league data
@@ -89,10 +90,11 @@ export default function RaceResultsPage() {
// Load penalties for this race
try {
const penaltiesQuery = getGetRacePenaltiesQuery();
const penaltiesData = await penaltiesQuery.execute(raceId);
const penaltiesUseCase = getGetRacePenaltiesUseCase();
await penaltiesUseCase.execute(raceId);
const penaltiesViewModel = penaltiesUseCase.presenter.getViewModel();
// Map the DTO to the PenaltyData interface expected by ResultsTable
setPenalties(penaltiesData.map(p => ({
setPenalties(penaltiesViewModel.map(p => ({
driverId: p.driverId,
type: p.type,
value: p.value,

View File

@@ -7,8 +7,7 @@ import Card from '@/components/ui/Card';
import Button from '@/components/ui/Button';
import Heading from '@/components/ui/Heading';
import { Race, RaceStatus } from '@gridpilot/racing/domain/entities/Race';
import { League } from '@gridpilot/racing/domain/entities/League';
import { getRaceRepository, getLeagueRepository } from '@/lib/di-container';
import { getGetRacesPageDataUseCase } from '@/lib/di-container';
import {
Calendar,
Clock,
@@ -32,8 +31,13 @@ type TimeFilter = 'all' | 'upcoming' | 'live' | 'past';
export default function RacesPage() {
const router = useRouter();
const [races, setRaces] = useState<Race[]>([]);
const [leagues, setLeagues] = useState<Map<string, League>>(new Map());
const [pageData, setPageData] = useState<{
races: Array<{ race: Race; leagueName: string }>;
stats: { total: number; scheduled: number; running: number; completed: number };
liveRaces: Array<{ race: Race; leagueName: string }>;
upcomingRaces: Array<{ race: Race; leagueName: string }>;
recentResults: Array<{ race: Race; leagueName: string }>;
} | null>(null);
const [loading, setLoading] = useState(true);
// Filters
@@ -43,19 +47,74 @@ export default function RacesPage() {
const loadRaces = async () => {
try {
const raceRepo = getRaceRepository();
const leagueRepo = getLeagueRepository();
const [allRaces, allLeagues] = await Promise.all([
raceRepo.findAll(),
leagueRepo.findAll()
]);
setRaces(allRaces.sort((a, b) => a.scheduledAt.getTime() - b.scheduledAt.getTime()));
const useCase = getGetRacesPageDataUseCase();
await useCase.execute();
const data = useCase.presenter.getViewModel();
const leagueMap = new Map<string, League>();
allLeagues.forEach(league => leagueMap.set(league.id, league));
setLeagues(leagueMap);
// Transform ViewModel back to page format
setPageData({
races: data.races.map(r => ({
race: {
id: r.id,
track: r.track,
car: r.car,
scheduledAt: new Date(r.scheduledAt),
status: r.status,
leagueId: r.leagueId,
strengthOfField: r.strengthOfField,
isUpcoming: () => r.isUpcoming,
isLive: () => r.isLive,
isPast: () => r.isPast,
} as Race,
leagueName: r.leagueName,
})),
stats: data.stats,
liveRaces: data.liveRaces.map(r => ({
race: {
id: r.id,
track: r.track,
car: r.car,
scheduledAt: new Date(r.scheduledAt),
status: r.status,
leagueId: r.leagueId,
strengthOfField: r.strengthOfField,
isUpcoming: () => r.isUpcoming,
isLive: () => r.isLive,
isPast: () => r.isPast,
} as Race,
leagueName: r.leagueName,
})),
upcomingRaces: data.upcomingThisWeek.map(r => ({
race: {
id: r.id,
track: r.track,
car: r.car,
scheduledAt: new Date(r.scheduledAt),
status: r.status,
leagueId: r.leagueId,
strengthOfField: r.strengthOfField,
isUpcoming: () => r.isUpcoming,
isLive: () => r.isLive,
isPast: () => r.isPast,
} as Race,
leagueName: r.leagueName,
})),
recentResults: data.recentResults.map(r => ({
race: {
id: r.id,
track: r.track,
car: r.car,
scheduledAt: new Date(r.scheduledAt),
status: r.status,
leagueId: r.leagueId,
strengthOfField: r.strengthOfField,
isUpcoming: () => r.isUpcoming,
isLive: () => r.isLive,
isPast: () => r.isPast,
} as Race,
leagueName: r.leagueName,
})),
});
} catch (err) {
console.error('Failed to load races:', err);
} finally {
@@ -69,7 +128,9 @@ export default function RacesPage() {
// Filter races
const filteredRaces = useMemo(() => {
return races.filter(race => {
if (!pageData) return [];
return pageData.races.filter(({ race }) => {
// Status filter
if (statusFilter !== 'all' && race.status !== statusFilter) {
return false;
@@ -93,53 +154,25 @@ export default function RacesPage() {
return true;
});
}, [races, statusFilter, leagueFilter, timeFilter]);
}, [pageData, statusFilter, leagueFilter, timeFilter]);
// Group races by date for calendar view
const racesByDate = useMemo(() => {
const grouped = new Map<string, Race[]>();
filteredRaces.forEach(race => {
const dateKey = race.scheduledAt.toISOString().split('T')[0];
const grouped = new Map<string, Array<{ race: Race; leagueName: string }>>();
filteredRaces.forEach(item => {
const dateKey = item.race.scheduledAt.toISOString().split('T')[0];
if (!grouped.has(dateKey)) {
grouped.set(dateKey, []);
}
grouped.get(dateKey)!.push(race);
grouped.get(dateKey)!.push(item);
});
return grouped;
}, [filteredRaces]);
// Get upcoming races (next 7 days)
const upcomingRaces = useMemo(() => {
const now = new Date();
const nextWeek = new Date(now.getTime() + 7 * 24 * 60 * 60 * 1000);
return races.filter(race =>
race.isUpcoming() &&
race.scheduledAt >= now &&
race.scheduledAt <= nextWeek
).slice(0, 5);
}, [races]);
// Get live races
const liveRaces = useMemo(() => {
return races.filter(race => race.isLive());
}, [races]);
// Get recent results
const recentResults = useMemo(() => {
return races
.filter(race => race.status === 'completed')
.sort((a, b) => b.scheduledAt.getTime() - a.scheduledAt.getTime())
.slice(0, 3);
}, [races]);
// Stats
const stats = useMemo(() => {
const total = races.length;
const scheduled = races.filter(r => r.status === 'scheduled').length;
const running = races.filter(r => r.status === 'running').length;
const completed = races.filter(r => r.status === 'completed').length;
return { total, scheduled, running, completed };
}, [races]);
const upcomingRaces = pageData?.upcomingRaces ?? [];
const liveRaces = pageData?.liveRaces ?? [];
const recentResults = pageData?.recentResults ?? [];
const stats = pageData?.stats ?? { total: 0, scheduled: 0, running: 0, completed: 0 };
const formatDate = (date: Date) => {
return new Date(date).toLocaleDateString('en-US', {
@@ -298,8 +331,8 @@ export default function RacesPage() {
</div>
<div className="space-y-3">
{liveRaces.map(race => (
<div
{liveRaces.map(({ race, leagueName }) => (
<div
key={race.id}
onClick={() => router.push(`/races/${race.id}`)}
className="flex items-center justify-between p-4 bg-deep-graphite/80 rounded-lg border border-performance-green/20 cursor-pointer hover:border-performance-green/40 transition-all"
@@ -310,7 +343,7 @@ export default function RacesPage() {
</div>
<div>
<h3 className="font-semibold text-white">{race.track}</h3>
<p className="text-sm text-gray-400">{leagues.get(race.leagueId)?.name}</p>
<p className="text-sm text-gray-400">{leagueName}</p>
</div>
</div>
<ChevronRight className="w-5 h-5 text-gray-400" />
@@ -352,11 +385,14 @@ export default function RacesPage() {
className="px-4 py-2 bg-deep-graphite border border-charcoal-outline rounded-lg text-white text-sm focus:outline-none focus:ring-2 focus:ring-primary-blue"
>
<option value="all">All Leagues</option>
{Array.from(leagues.values()).map(league => (
<option key={league.id} value={league.id}>
{league.name}
</option>
))}
{pageData && [...new Set(pageData.races.map(r => r.race.leagueId))].map(leagueId => {
const item = pageData.races.find(r => r.race.leagueId === leagueId);
return item ? (
<option key={leagueId} value={leagueId}>
{item.leagueName}
</option>
) : null;
})}
</select>
</div>
</Card>
@@ -371,7 +407,7 @@ export default function RacesPage() {
<div>
<p className="text-white font-medium mb-1">No races found</p>
<p className="text-sm text-gray-500">
{races.length === 0
{pageData?.races.length === 0
? 'No races have been scheduled yet'
: 'Try adjusting your filters'}
</p>
@@ -397,10 +433,9 @@ export default function RacesPage() {
{/* Races for this date */}
<div className="space-y-2">
{dayRaces.map(race => {
{dayRaces.map(({ race, leagueName }) => {
const config = statusConfig[race.status];
const StatusIcon = config.icon;
const league = leagues.get(race.leagueId);
return (
<div
@@ -456,19 +491,17 @@ export default function RacesPage() {
</div>
{/* League Link */}
{league && (
<div className="mt-3 pt-3 border-t border-charcoal-outline/50">
<Link
href={`/leagues/${league.id}`}
onClick={(e) => e.stopPropagation()}
className="inline-flex items-center gap-2 text-sm text-primary-blue hover:underline"
>
<Trophy className="w-3.5 h-3.5" />
{league.name}
<ArrowRight className="w-3 h-3" />
</Link>
</div>
)}
<div className="mt-3 pt-3 border-t border-charcoal-outline/50">
<Link
href={`/leagues/${race.leagueId}`}
onClick={(e) => e.stopPropagation()}
className="inline-flex items-center gap-2 text-sm text-primary-blue hover:underline"
>
<Trophy className="w-3.5 h-3.5" />
{leagueName}
<ArrowRight className="w-3 h-3" />
</Link>
</div>
</div>
{/* Arrow */}
@@ -515,8 +548,8 @@ export default function RacesPage() {
</p>
) : (
<div className="space-y-3">
{upcomingRaces.map((race, index) => (
<div
{upcomingRaces.map(({ race }) => (
<div
key={race.id}
onClick={() => router.push(`/races/${race.id}`)}
className="flex items-center gap-3 p-2 rounded-lg hover:bg-deep-graphite cursor-pointer transition-colors"
@@ -552,8 +585,8 @@ export default function RacesPage() {
</p>
) : (
<div className="space-y-3">
{recentResults.map(race => (
<div
{recentResults.map(({ race }) => (
<div
key={race.id}
onClick={() => router.push(`/races/${race.id}/results`)}
className="flex items-center gap-3 p-2 rounded-lg hover:bg-deep-graphite cursor-pointer transition-colors"

View File

@@ -32,57 +32,73 @@ interface Sponsorship {
drivers: number;
}
// Mock data - in production would come from repository
const MOCK_SPONSORSHIPS: Sponsorship[] = [
{
id: 'sp-1',
leagueId: 'league-1',
leagueName: 'GT3 Pro Championship',
tier: 'main',
status: 'active',
startDate: new Date('2025-01-01'),
endDate: new Date('2025-06-30'),
price: 1200,
impressions: 45200,
drivers: 32,
},
{
id: 'sp-2',
leagueId: 'league-2',
leagueName: 'Endurance Masters',
tier: 'main',
status: 'active',
startDate: new Date('2025-02-01'),
endDate: new Date('2025-07-31'),
price: 1000,
impressions: 38100,
drivers: 48,
},
{
id: 'sp-3',
leagueId: 'league-3',
leagueName: 'Formula Sim Series',
tier: 'secondary',
status: 'active',
startDate: new Date('2025-03-01'),
endDate: new Date('2025-08-31'),
price: 400,
impressions: 22800,
drivers: 24,
},
{
id: 'sp-4',
leagueId: 'league-4',
leagueName: 'Touring Car Cup',
tier: 'secondary',
status: 'pending',
startDate: new Date('2025-04-01'),
endDate: new Date('2025-09-30'),
price: 350,
impressions: 0,
drivers: 28,
},
];
interface SponsorshipDetailApi {
id: string;
leagueId: string;
leagueName: string;
seasonId: string;
seasonName: string;
seasonStartDate?: string;
seasonEndDate?: string;
tier: 'main' | 'secondary';
status: string;
pricing: {
amount: number;
currency: string;
};
metrics: {
drivers: number;
races: number;
completedRaces: number;
impressions: number;
};
createdAt: string;
activatedAt?: string;
}
interface SponsorSponsorshipsResponse {
sponsorId: string;
sponsorName: string;
sponsorships: SponsorshipDetailApi[];
summary: {
totalSponsorships: number;
activeSponsorships: number;
totalInvestment: number;
totalPlatformFees: number;
currency: string;
};
}
function mapSponsorshipStatus(status: string): 'active' | 'pending' | 'expired' {
switch (status) {
case 'active':
return 'active';
case 'pending':
return 'pending';
default:
return 'expired';
}
}
function mapApiToSponsorships(response: SponsorSponsorshipsResponse): Sponsorship[] {
return response.sponsorships.map((s) => {
const start = s.seasonStartDate ? new Date(s.seasonStartDate) : new Date(s.createdAt);
const end = s.seasonEndDate ? new Date(s.seasonEndDate) : start;
return {
id: s.id,
leagueId: s.leagueId,
leagueName: s.leagueName,
tier: s.tier,
status: mapSponsorshipStatus(s.status),
startDate: start,
endDate: end,
price: s.pricing.amount,
impressions: s.metrics.impressions,
drivers: s.metrics.drivers,
};
});
}
function SponsorshipCard({ sponsorship }: { sponsorship: Sponsorship }) {
const router = useRouter();
@@ -179,18 +195,59 @@ function SponsorshipCard({ sponsorship }: { sponsorship: Sponsorship }) {
export default function SponsorCampaignsPage() {
const router = useRouter();
const [filter, setFilter] = useState<'all' | 'active' | 'pending' | 'expired'>('all');
const filteredSponsorships = filter === 'all'
? MOCK_SPONSORSHIPS
: MOCK_SPONSORSHIPS.filter(s => s.status === filter);
const [sponsorships, setSponsorships] = useState<Sponsorship[]>([]);
const [loading, setLoading] = useState(true);
useEffect(() => {
let isMounted = true;
async function fetchSponsorships() {
try {
const response = await fetch('/api/sponsors/sponsorships');
if (!response.ok) {
if (!isMounted) return;
setSponsorships([]);
return;
}
const json: SponsorSponsorshipsResponse = await response.json();
if (!isMounted) return;
setSponsorships(mapApiToSponsorships(json));
} catch {
if (!isMounted) return;
setSponsorships([]);
} finally {
if (isMounted) {
setLoading(false);
}
}
}
fetchSponsorships();
return () => {
isMounted = false;
};
}, []);
const filteredSponsorships = filter === 'all'
? sponsorships
: sponsorships.filter(s => s.status === filter);
const stats = {
total: MOCK_SPONSORSHIPS.length,
active: MOCK_SPONSORSHIPS.filter(s => s.status === 'active').length,
pending: MOCK_SPONSORSHIPS.filter(s => s.status === 'pending').length,
totalInvestment: MOCK_SPONSORSHIPS.reduce((sum, s) => sum + s.price, 0),
total: sponsorships.length,
active: sponsorships.filter(s => s.status === 'active').length,
pending: sponsorships.filter(s => s.status === 'pending').length,
totalInvestment: sponsorships.reduce((sum, s) => sum + s.price, 0),
};
if (loading) {
return (
<div className="max-w-6xl mx-auto py-8 px-4">
<p className="text-gray-400">Loading sponsorships</p>
</div>
);
}
return (
<div className="max-w-6xl mx-auto py-8 px-4">
{/* Header */}

View File

@@ -52,64 +52,6 @@ interface SponsorDashboardData {
};
}
// Fallback mock data for demo mode
const MOCK_DASHBOARD: SponsorDashboardData = {
sponsorId: 'demo-sponsor',
sponsorName: 'Demo Sponsor',
metrics: {
impressions: 124500,
impressionsChange: 12.5,
uniqueViewers: 8420,
viewersChange: 8.3,
races: 24,
drivers: 156,
exposure: 87.5,
exposureChange: 5.2,
},
sponsoredLeagues: [
{
id: 'league-1',
name: 'GT3 Pro Championship',
tier: 'main',
drivers: 32,
races: 12,
impressions: 45200,
status: 'active',
},
{
id: 'league-2',
name: 'Endurance Masters',
tier: 'main',
drivers: 48,
races: 6,
impressions: 38100,
status: 'active',
},
{
id: 'league-3',
name: 'Formula Sim Series',
tier: 'secondary',
drivers: 24,
races: 8,
impressions: 22800,
status: 'active',
},
{
id: 'league-4',
name: 'Touring Car Cup',
tier: 'secondary',
drivers: 28,
races: 10,
impressions: 18400,
status: 'upcoming',
},
],
investment: {
activeSponsorships: 4,
totalInvestment: 2400,
costPerThousandViews: 19.28,
},
};
function MetricCard({
title,
@@ -205,15 +147,13 @@ export default function SponsorDashboardPage() {
try {
const response = await fetch('/api/sponsors/dashboard');
if (response.ok) {
const dashboardData = await response.json();
const dashboardData: SponsorDashboardData = await response.json();
setData(dashboardData);
} else {
// Use mock data for demo mode
setData(MOCK_DASHBOARD);
setError('Failed to load sponsor dashboard');
}
} catch {
// Use mock data on error
setData(MOCK_DASHBOARD);
setError('Failed to load sponsor dashboard');
} finally {
setLoading(false);
}
@@ -230,7 +170,19 @@ export default function SponsorDashboardPage() {
);
}
const dashboardData = data || MOCK_DASHBOARD;
if (!data) {
return (
<div className="max-w-7xl mx-auto py-8 px-4">
<div className="rounded-lg bg-warning-amber/10 border border-warning-amber/30 p-4">
<p className="text-sm text-warning-amber">
{error ?? 'No sponsor dashboard data available yet.'}
</p>
</div>
</div>
);
}
const dashboardData = data;
return (
<div className="max-w-7xl mx-auto py-8 px-4">

View File

@@ -14,8 +14,8 @@ import TeamStandings from '@/components/teams/TeamStandings';
import TeamAdmin from '@/components/teams/TeamAdmin';
import JoinTeamButton from '@/components/teams/JoinTeamButton';
import {
getGetTeamDetailsQuery,
getGetTeamMembersQuery,
getGetTeamDetailsUseCase,
getGetTeamMembersUseCase,
getTeamMembershipRepository,
} from '@/lib/di-container';
import { useEffectiveDriverId } from '@/lib/currentDriver';
@@ -39,11 +39,15 @@ export default function TeamDetailPage() {
const loadTeamData = useCallback(async () => {
setLoading(true);
try {
const detailsQuery = getGetTeamDetailsQuery();
const membersQuery = getGetTeamMembersQuery();
const detailsUseCase = getGetTeamDetailsUseCase();
const membersUseCase = getGetTeamMembersUseCase();
const details = await detailsQuery.execute({ teamId, driverId: currentDriverId });
const teamMemberships = await membersQuery.execute({ teamId });
await detailsUseCase.execute({ teamId, driverId: currentDriverId });
const detailsViewModel = detailsUseCase.presenter.getViewModel();
await membersUseCase.execute({ teamId });
const membersViewModel = membersUseCase.presenter.getViewModel();
const teamMemberships = membersViewModel.members;
const adminStatus =
teamMemberships.some(
@@ -52,7 +56,7 @@ export default function TeamDetailPage() {
(m.role === 'owner' || m.role === 'manager'),
) ?? false;
setTeam(details.team);
setTeam(detailsViewModel.team);
setMemberships(teamMemberships);
setIsAdmin(adminStatus);
} finally {

View File

@@ -22,7 +22,7 @@ import {
import Button from '@/components/ui/Button';
import Input from '@/components/ui/Input';
import Heading from '@/components/ui/Heading';
import { getGetAllTeamsQuery, getGetTeamMembersQuery, getDriverStats } from '@/lib/di-container';
import { getGetAllTeamsUseCase, getGetTeamMembersUseCase, getDriverStats } from '@/lib/di-container';
import type { Team } from '@gridpilot/racing';
// ============================================================================
@@ -260,15 +260,19 @@ export default function TeamLeaderboardPage() {
const loadTeams = async () => {
try {
const allTeamsQuery = getGetAllTeamsQuery();
const teamMembersQuery = getGetTeamMembersQuery();
const allTeamsUseCase = getGetAllTeamsUseCase();
const teamMembersUseCase = getGetTeamMembersUseCase();
const allTeams = await allTeamsQuery.execute();
await allTeamsUseCase.execute();
const allTeamsViewModel = allTeamsUseCase.presenter.getViewModel();
const allTeams = allTeamsViewModel.teams;
const teamData: TeamDisplayData[] = [];
await Promise.all(
allTeams.map(async (team: Team) => {
const memberships = await teamMembersQuery.execute({ teamId: team.id });
await teamMembersUseCase.execute({ teamId: team.id });
const membershipsViewModel = teamMembersUseCase.presenter.getViewModel();
const memberships = membershipsViewModel.members;
const memberCount = memberships.length;
let ratingSum = 0;

View File

@@ -28,30 +28,14 @@ import Card from '@/components/ui/Card';
import Input from '@/components/ui/Input';
import Heading from '@/components/ui/Heading';
import CreateTeamForm from '@/components/teams/CreateTeamForm';
import { getGetAllTeamsQuery, getGetTeamMembersQuery, getDriverStats } from '@/lib/di-container';
import type { Team } from '@gridpilot/racing';
import { getGetTeamsLeaderboardUseCase } from '@/lib/di-container';
import type { TeamLeaderboardItemViewModel, SkillLevel } from '@gridpilot/racing/application/presenters/ITeamsLeaderboardPresenter';
// ============================================================================
// TYPES
// ============================================================================
type SkillLevel = 'beginner' | 'intermediate' | 'advanced' | 'pro';
interface TeamDisplayData {
id: string;
name: string;
memberCount: number;
rating: number | null;
totalWins: number;
totalRaces: number;
performanceLevel: SkillLevel;
isRecruiting: boolean;
createdAt: Date;
description?: string;
specialization?: 'endurance' | 'sprint' | 'mixed';
region?: string;
languages?: string[];
}
type TeamDisplayData = TeamLeaderboardItemViewModel;
// ============================================================================
// SKILL LEVEL CONFIG
@@ -463,59 +447,10 @@ export default function TeamsPage() {
const loadTeams = async () => {
try {
const allTeamsQuery = getGetAllTeamsQuery();
const teamMembersQuery = getGetTeamMembersQuery();
const allTeams = await allTeamsQuery.execute();
const teamData: TeamDisplayData[] = [];
await Promise.all(
allTeams.map(async (team: Team) => {
const memberships = await teamMembersQuery.execute({ teamId: team.id });
const memberCount = memberships.length;
let ratingSum = 0;
let ratingCount = 0;
let totalWins = 0;
let totalRaces = 0;
for (const membership of memberships) {
const stats = getDriverStats(membership.driverId);
if (!stats) continue;
if (typeof stats.rating === 'number') {
ratingSum += stats.rating;
ratingCount += 1;
}
totalWins += stats.wins ?? 0;
totalRaces += stats.totalRaces ?? 0;
}
const averageRating = ratingCount > 0 ? ratingSum / ratingCount : null;
let performanceLevel: TeamDisplayData['performanceLevel'] = 'beginner';
if (averageRating !== null) {
if (averageRating >= 4500) performanceLevel = 'pro';
else if (averageRating >= 3000) performanceLevel = 'advanced';
else if (averageRating >= 2000) performanceLevel = 'intermediate';
}
teamData.push({
id: team.id,
name: team.name,
memberCount,
rating: averageRating,
totalWins,
totalRaces,
performanceLevel,
isRecruiting: true,
createdAt: new Date(),
});
}),
);
setRealTeams(teamData);
const useCase = getGetTeamsLeaderboardUseCase();
await useCase.execute();
const viewModel = useCase.presenter.getViewModel();
setRealTeams(viewModel.teams);
} catch (error) {
console.error('Failed to load teams:', error);
} finally {

View File

@@ -8,7 +8,7 @@ import CareerHighlights from './CareerHighlights';
import DriverRankings from './DriverRankings';
import PerformanceMetrics from './PerformanceMetrics';
import { useEffect, useState } from 'react';
import { getDriverStats, getLeagueRankings, getGetDriverTeamQuery, getAllDriverRankings } from '@/lib/di-container';
import { getDriverStats, getLeagueRankings, getGetDriverTeamUseCase, getAllDriverRankings } from '@/lib/di-container';
import { getPrimaryLeagueIdForDriver } from '@/lib/leagueMembership';
import type { GetDriverTeamQueryResultDTO } from '@gridpilot/racing/application/dto/TeamCommandAndQueryDTO';
@@ -29,9 +29,10 @@ export default function DriverProfile({ driver, isOwnProfile = false, onEditClic
useEffect(() => {
const load = async () => {
const query = getGetDriverTeamQuery();
const result = await query.execute({ driverId: driver.id });
setTeamData(result);
const useCase = getGetDriverTeamUseCase();
await useCase.execute({ driverId: driver.id });
const viewModel = useCase.presenter.getViewModel();
setTeamData(viewModel.result);
};
void load();
}, [driver.id]);

View File

@@ -6,7 +6,7 @@ import Input from '../ui/Input';
import { DollarSign, Star, Award, Plus, X, Bell } from 'lucide-react';
import PendingSponsorshipRequests, { type PendingRequestDTO } from '../sponsors/PendingSponsorshipRequests';
import {
getGetPendingSponsorshipRequestsQuery,
getGetPendingSponsorshipRequestsUseCase,
getAcceptSponsorshipRequestUseCase,
getRejectSponsorshipRequestUseCase,
getSeasonRepository,
@@ -71,8 +71,8 @@ export function LeagueSponsorshipsSection({
setRequestsLoading(true);
try {
const query = getGetPendingSponsorshipRequestsQuery();
const result = await query.execute({
const useCase = getGetPendingSponsorshipRequestsUseCase();
await useCase.execute({
entityType: 'season',
entityId: seasonId,
});

View File

@@ -5,7 +5,7 @@ import Button from '@/components/ui/Button';
import {
getJoinTeamUseCase,
getLeaveTeamUseCase,
getGetDriverTeamQuery,
getGetDriverTeamUseCase,
getTeamMembershipRepository,
} from '@/lib/di-container';
import { useEffectiveDriverId } from '@/lib/currentDriver';
@@ -34,11 +34,12 @@ export default function JoinTeamButton({
const m = await membershipRepo.getMembership(teamId, currentDriverId);
setMembership(m);
const driverTeamQuery = getGetDriverTeamQuery();
const driverTeam = await driverTeamQuery.execute({ driverId: currentDriverId });
if (driverTeam) {
setCurrentTeamId(driverTeam.team.id);
setCurrentTeamName(driverTeam.team.name);
const driverTeamUseCase = getGetDriverTeamUseCase();
await driverTeamUseCase.execute({ driverId: currentDriverId });
const viewModel = driverTeamUseCase.presenter.getViewModel();
if (viewModel.result) {
setCurrentTeamId(viewModel.result.team.id);
setCurrentTeamName(viewModel.result.team.name);
} else {
setCurrentTeamId(null);
setCurrentTeamName(null);

View File

@@ -6,7 +6,7 @@ import Button from '@/components/ui/Button';
import Input from '@/components/ui/Input';
import {
getDriverRepository,
getGetTeamJoinRequestsQuery,
getGetTeamJoinRequestsUseCase,
getApproveTeamJoinRequestUseCase,
getRejectTeamJoinRequestUseCase,
getUpdateTeamUseCase,
@@ -36,15 +36,16 @@ export default function TeamAdmin({ team, onUpdate }: TeamAdminProps) {
}, [team.id]);
const loadJoinRequests = async () => {
const query = getGetTeamJoinRequestsQuery();
const requests = await query.execute({ teamId: team.id });
setJoinRequests(requests);
const useCase = getGetTeamJoinRequestsUseCase();
await useCase.execute({ teamId: team.id });
const viewModel = useCase.presenter.getViewModel();
setJoinRequests(viewModel.requests);
const driverRepo = getDriverRepository();
const allDrivers = await driverRepo.findAll();
const driverMap: Record<string, DriverDTO> = {};
for (const request of requests) {
for (const request of viewModel.requests) {
const driver = allDrivers.find(d => d.id === request.driverId);
if (driver) {
const dto = EntityMappers.toDriverDTO(driver);

View File

@@ -88,48 +88,77 @@ import {
JoinLeagueUseCase,
RegisterForRaceUseCase,
WithdrawFromRaceUseCase,
IsDriverRegisteredForRaceQuery,
GetRaceRegistrationsQuery,
CreateTeamUseCase,
JoinTeamUseCase,
LeaveTeamUseCase,
ApproveTeamJoinRequestUseCase,
RejectTeamJoinRequestUseCase,
UpdateTeamUseCase,
GetAllTeamsQuery,
GetTeamDetailsQuery,
GetTeamMembersQuery,
GetTeamJoinRequestsQuery,
GetDriverTeamQuery,
GetLeagueStandingsQuery,
GetLeagueDriverSeasonStatsQuery,
GetAllLeaguesWithCapacityQuery,
GetAllLeaguesWithCapacityAndScoringQuery,
ListLeagueScoringPresetsQuery,
GetLeagueScoringConfigQuery,
GetAllTeamsUseCase,
GetTeamDetailsUseCase,
GetTeamMembersUseCase,
GetTeamJoinRequestsUseCase,
GetDriverTeamUseCase,
CreateLeagueWithSeasonAndScoringUseCase,
GetLeagueFullConfigQuery,
GetRaceWithSOFQuery,
GetLeagueStatsQuery,
FileProtestUseCase,
ReviewProtestUseCase,
ApplyPenaltyUseCase,
GetRaceProtestsQuery,
GetRacePenaltiesQuery,
RequestProtestDefenseUseCase,
SubmitProtestDefenseUseCase,
GetSponsorDashboardQuery,
GetSponsorSponsorshipsQuery,
GetPendingSponsorshipRequestsQuery,
GetEntitySponsorshipPricingQuery,
GetSponsorDashboardUseCase,
GetSponsorSponsorshipsUseCase,
GetPendingSponsorshipRequestsUseCase,
GetEntitySponsorshipPricingUseCase,
ApplyForSponsorshipUseCase,
AcceptSponsorshipRequestUseCase,
RejectSponsorshipRequestUseCase,
} from '@gridpilot/racing/application';
import { IsDriverRegisteredForRaceUseCase } from '@gridpilot/racing/application/use-cases/IsDriverRegisteredForRaceQuery';
import { GetRaceRegistrationsUseCase } from '@gridpilot/racing/application/use-cases/GetRaceRegistrationsQuery';
import { GetRaceWithSOFUseCase } from '@gridpilot/racing/application/use-cases/GetRaceWithSOFQuery';
import { GetRaceProtestsUseCase } from '@gridpilot/racing/application/use-cases/GetRaceProtestsQuery';
import { GetRacePenaltiesUseCase } from '@gridpilot/racing/application/use-cases/GetRacePenaltiesQuery';
import { GetLeagueStandingsUseCase } from '@gridpilot/racing/application/use-cases/GetLeagueStandingsQuery';
import { GetLeagueDriverSeasonStatsUseCase } from '@gridpilot/racing/application/use-cases/GetLeagueDriverSeasonStatsQuery';
import { GetAllLeaguesWithCapacityUseCase } from '@gridpilot/racing/application/use-cases/GetAllLeaguesWithCapacityQuery';
import { GetAllLeaguesWithCapacityAndScoringUseCase } from '@gridpilot/racing/application/use-cases/GetAllLeaguesWithCapacityAndScoringQuery';
import { ListLeagueScoringPresetsUseCase } from '@gridpilot/racing/application/use-cases/ListLeagueScoringPresetsQuery';
import { GetLeagueScoringConfigUseCase } from '@gridpilot/racing/application/use-cases/GetLeagueScoringConfigQuery';
import { GetLeagueFullConfigUseCase } from '@gridpilot/racing/application/use-cases/GetLeagueFullConfigQuery';
import { GetLeagueStatsUseCase } from '@gridpilot/racing/application/use-cases/GetLeagueStatsQuery';
import { GetRacesPageDataUseCase } from '@gridpilot/racing/application/use-cases/GetRacesPageDataUseCase';
import { GetDriversLeaderboardUseCase } from '@gridpilot/racing/application/use-cases/GetDriversLeaderboardUseCase';
import { GetTeamsLeaderboardUseCase } from '@gridpilot/racing/application/use-cases/GetTeamsLeaderboardUseCase';
import { TransferLeagueOwnershipUseCase } from '@gridpilot/racing/application/use-cases/TransferLeagueOwnershipUseCase';
import { DriversLeaderboardPresenter } from '../../lib/presenters/DriversLeaderboardPresenter';
import { TeamsLeaderboardPresenter } from '../../lib/presenters/TeamsLeaderboardPresenter';
import { RacesPagePresenter } from '../../lib/presenters/RacesPagePresenter';
import { AllTeamsPresenter } from '../../lib/presenters/AllTeamsPresenter';
import { TeamDetailsPresenter } from '../../lib/presenters/TeamDetailsPresenter';
import { TeamMembersPresenter } from '../../lib/presenters/TeamMembersPresenter';
import { TeamJoinRequestsPresenter } from '../../lib/presenters/TeamJoinRequestsPresenter';
import { DriverTeamPresenter } from '../../lib/presenters/DriverTeamPresenter';
import { AllLeaguesWithCapacityPresenter } from '../../lib/presenters/AllLeaguesWithCapacityPresenter';
import { AllLeaguesWithCapacityAndScoringPresenter } from '../../lib/presenters/AllLeaguesWithCapacityAndScoringPresenter';
import { LeagueStatsPresenter } from '../../lib/presenters/LeagueStatsPresenter';
import { LeagueScoringConfigPresenter } from '../../lib/presenters/LeagueScoringConfigPresenter';
import { LeagueFullConfigPresenter } from '../../lib/presenters/LeagueFullConfigPresenter';
import { LeagueDriverSeasonStatsPresenter } from '../../lib/presenters/LeagueDriverSeasonStatsPresenter';
import { LeagueStandingsPresenter } from '../../lib/presenters/LeagueStandingsPresenter';
import { LeagueScoringPresetsPresenter } from '../../lib/presenters/LeagueScoringPresetsPresenter';
import { RaceWithSOFPresenter } from '../../lib/presenters/RaceWithSOFPresenter';
import { RaceProtestsPresenter } from '../../lib/presenters/RaceProtestsPresenter';
import { RacePenaltiesPresenter } from '../../lib/presenters/RacePenaltiesPresenter';
import { RaceRegistrationsPresenter } from '../../lib/presenters/RaceRegistrationsPresenter';
import { DriverRegistrationStatusPresenter } from '../../lib/presenters/DriverRegistrationStatusPresenter';
import type { DriverRatingProvider } from '@gridpilot/racing/application';
import type { LeagueScoringPresetProvider } from '@gridpilot/racing/application/ports/LeagueScoringPresetProvider';
import { PreviewLeagueScheduleQuery } from '@gridpilot/racing/application';
import { PreviewLeagueScheduleUseCase } from '@gridpilot/racing/application';
import { SponsorDashboardPresenter } from '../../lib/presenters/SponsorDashboardPresenter';
import { SponsorSponsorshipsPresenter } from '../../lib/presenters/SponsorSponsorshipsPresenter';
import { PendingSponsorshipRequestsPresenter } from '../../lib/presenters/PendingSponsorshipRequestsPresenter';
import { EntitySponsorshipPricingPresenter } from '../../lib/presenters/EntitySponsorshipPricingPresenter';
import { LeagueSchedulePreviewPresenter } from '../../lib/presenters/LeagueSchedulePreviewPresenter';
// Testing support
import {
@@ -840,24 +869,28 @@ export function configureDIContainer(): void {
);
// Register queries - Racing
const driverRegistrationStatusPresenter = new DriverRegistrationStatusPresenter();
container.registerInstance(
DI_TOKENS.IsDriverRegisteredForRaceQuery,
new IsDriverRegisteredForRaceQuery(raceRegistrationRepository)
DI_TOKENS.IsDriverRegisteredForRaceUseCase,
new IsDriverRegisteredForRaceUseCase(raceRegistrationRepository, driverRegistrationStatusPresenter)
);
const raceRegistrationsPresenter = new RaceRegistrationsPresenter();
container.registerInstance(
DI_TOKENS.GetRaceRegistrationsQuery,
new GetRaceRegistrationsQuery(raceRegistrationRepository)
DI_TOKENS.GetRaceRegistrationsUseCase,
new GetRaceRegistrationsUseCase(raceRegistrationRepository, raceRegistrationsPresenter)
);
const leagueStandingsPresenter = new LeagueStandingsPresenter();
container.registerInstance(
DI_TOKENS.GetLeagueStandingsQuery,
new GetLeagueStandingsQuery(standingRepository)
DI_TOKENS.GetLeagueStandingsUseCase,
new GetLeagueStandingsUseCase(standingRepository, leagueStandingsPresenter)
);
const leagueDriverSeasonStatsPresenter = new LeagueDriverSeasonStatsPresenter();
container.registerInstance(
DI_TOKENS.GetLeagueDriverSeasonStatsQuery,
new GetLeagueDriverSeasonStatsQuery(
DI_TOKENS.GetLeagueDriverSeasonStatsUseCase,
new GetLeagueDriverSeasonStatsUseCase(
standingRepository,
resultRepository,
penaltyRepository,
@@ -875,113 +908,200 @@ export function configureDIContainer(): void {
ratingChange: delta !== 0 ? delta : null,
};
},
}
},
leagueDriverSeasonStatsPresenter
)
);
const allLeaguesWithCapacityPresenter = new AllLeaguesWithCapacityPresenter();
container.registerInstance(
DI_TOKENS.GetAllLeaguesWithCapacityQuery,
new GetAllLeaguesWithCapacityQuery(leagueRepository, leagueMembershipRepository)
DI_TOKENS.GetAllLeaguesWithCapacityUseCase,
new GetAllLeaguesWithCapacityUseCase(
leagueRepository,
leagueMembershipRepository,
allLeaguesWithCapacityPresenter
)
);
const allLeaguesWithCapacityAndScoringPresenter = new AllLeaguesWithCapacityAndScoringPresenter();
container.registerInstance(
DI_TOKENS.GetAllLeaguesWithCapacityAndScoringQuery,
new GetAllLeaguesWithCapacityAndScoringQuery(
DI_TOKENS.GetAllLeaguesWithCapacityAndScoringUseCase,
new GetAllLeaguesWithCapacityAndScoringUseCase(
leagueRepository,
leagueMembershipRepository,
seasonRepository,
leagueScoringConfigRepository,
gameRepository,
leagueScoringPresetProvider
leagueScoringPresetProvider,
allLeaguesWithCapacityAndScoringPresenter
)
);
const leagueScoringPresetsPresenter = new LeagueScoringPresetsPresenter();
container.registerInstance(
DI_TOKENS.ListLeagueScoringPresetsQuery,
new ListLeagueScoringPresetsQuery(leagueScoringPresetProvider)
DI_TOKENS.ListLeagueScoringPresetsUseCase,
new ListLeagueScoringPresetsUseCase(leagueScoringPresetProvider, leagueScoringPresetsPresenter)
);
const leagueScoringConfigPresenter = new LeagueScoringConfigPresenter();
container.registerInstance(
DI_TOKENS.GetLeagueScoringConfigQuery,
new GetLeagueScoringConfigQuery(
DI_TOKENS.GetLeagueScoringConfigUseCase,
new GetLeagueScoringConfigUseCase(
leagueRepository,
seasonRepository,
leagueScoringConfigRepository,
gameRepository,
leagueScoringPresetProvider
leagueScoringPresetProvider,
leagueScoringConfigPresenter
)
);
const leagueFullConfigPresenter = new LeagueFullConfigPresenter();
container.registerInstance(
DI_TOKENS.GetLeagueFullConfigQuery,
new GetLeagueFullConfigQuery(
DI_TOKENS.GetLeagueFullConfigUseCase,
new GetLeagueFullConfigUseCase(
leagueRepository,
seasonRepository,
leagueScoringConfigRepository,
gameRepository
gameRepository,
leagueFullConfigPresenter
)
);
const leagueSchedulePreviewPresenter = new LeagueSchedulePreviewPresenter();
container.registerInstance(
DI_TOKENS.PreviewLeagueScheduleQuery,
new PreviewLeagueScheduleQuery()
DI_TOKENS.PreviewLeagueScheduleUseCase,
new PreviewLeagueScheduleUseCase(undefined, leagueSchedulePreviewPresenter)
);
const raceWithSOFPresenter = new RaceWithSOFPresenter();
container.registerInstance(
DI_TOKENS.GetRaceWithSOFQuery,
new GetRaceWithSOFQuery(
DI_TOKENS.GetRaceWithSOFUseCase,
new GetRaceWithSOFUseCase(
raceRepository,
raceRegistrationRepository,
resultRepository,
driverRatingProvider
driverRatingProvider,
raceWithSOFPresenter
)
);
const leagueStatsPresenter = new LeagueStatsPresenter();
container.registerInstance(
DI_TOKENS.GetLeagueStatsQuery,
new GetLeagueStatsQuery(
DI_TOKENS.GetLeagueStatsUseCase,
new GetLeagueStatsUseCase(
leagueRepository,
raceRepository,
resultRepository,
driverRatingProvider
driverRatingProvider,
leagueStatsPresenter
)
);
// Register queries - Teams
const racesPresenter = new RacesPagePresenter();
container.registerInstance(
DI_TOKENS.GetAllTeamsQuery,
new GetAllTeamsQuery(teamRepository)
DI_TOKENS.GetRacesPageDataUseCase,
new GetRacesPageDataUseCase(raceRepository, leagueRepository, racesPresenter)
);
// Create services for driver leaderboard query
const rankingService = {
getAllDriverRankings: () => {
const stats = getDIContainer().resolve<Record<string, any>>(DI_TOKENS.DriverStats);
return Object.entries(stats).map(([driverId, stat]) => ({
driverId,
rating: stat.rating,
overallRank: stat.overallRank,
})).sort((a, b) => b.rating - a.rating);
}
};
const driverStatsService = {
getDriverStats: (driverId: string) => {
const stats = getDIContainer().resolve<Record<string, any>>(DI_TOKENS.DriverStats);
return stats[driverId] || null;
}
};
const imageService = getDIContainer().resolve<ImageServicePort>(DI_TOKENS.ImageService);
const driversPresenter = new DriversLeaderboardPresenter();
container.registerInstance(
DI_TOKENS.GetTeamDetailsQuery,
new GetTeamDetailsQuery(teamRepository, teamMembershipRepository)
DI_TOKENS.GetDriversLeaderboardUseCase,
new GetDriversLeaderboardUseCase(
driverRepository,
rankingService as any,
driverStatsService as any,
imageService,
driversPresenter
)
);
const getDriverStatsAdapter = (driverId: string) => {
const stats = getDIContainer().resolve<Record<string, any>>(DI_TOKENS.DriverStats);
const stat = stats[driverId];
if (!stat) return null;
return {
rating: stat.rating ?? null,
wins: stat.wins ?? 0,
totalRaces: stat.totalRaces ?? 0,
};
};
const teamsPresenter = new TeamsLeaderboardPresenter();
container.registerInstance(
DI_TOKENS.GetTeamMembersQuery,
new GetTeamMembersQuery(teamMembershipRepository)
DI_TOKENS.GetTeamsLeaderboardUseCase,
new GetTeamsLeaderboardUseCase(
teamRepository,
teamMembershipRepository,
driverRepository,
getDriverStatsAdapter,
teamsPresenter
)
);
// Register use cases - Teams (Query-like with Presenters)
const allTeamsPresenter = new AllTeamsPresenter();
container.registerInstance(
DI_TOKENS.GetTeamJoinRequestsQuery,
new GetTeamJoinRequestsQuery(teamMembershipRepository)
DI_TOKENS.GetAllTeamsUseCase,
new GetAllTeamsUseCase(teamRepository, teamMembershipRepository, allTeamsPresenter)
);
const teamDetailsPresenter = new TeamDetailsPresenter();
container.registerInstance(
DI_TOKENS.GetDriverTeamQuery,
new GetDriverTeamQuery(teamRepository, teamMembershipRepository)
DI_TOKENS.GetTeamDetailsUseCase,
new GetTeamDetailsUseCase(teamRepository, teamMembershipRepository, teamDetailsPresenter)
);
const teamMembersPresenter = new TeamMembersPresenter();
container.registerInstance(
DI_TOKENS.GetTeamMembersUseCase,
new GetTeamMembersUseCase(teamMembershipRepository, driverRepository, imageService, teamMembersPresenter)
);
const teamJoinRequestsPresenter = new TeamJoinRequestsPresenter();
container.registerInstance(
DI_TOKENS.GetTeamJoinRequestsUseCase,
new GetTeamJoinRequestsUseCase(teamMembershipRepository, driverRepository, imageService, teamJoinRequestsPresenter)
);
const driverTeamPresenter = new DriverTeamPresenter();
container.registerInstance(
DI_TOKENS.GetDriverTeamUseCase,
new GetDriverTeamUseCase(teamRepository, teamMembershipRepository, driverTeamPresenter)
);
// Register queries - Stewarding
const raceProtestsPresenter = new RaceProtestsPresenter();
container.registerInstance(
DI_TOKENS.GetRaceProtestsQuery,
new GetRaceProtestsQuery(protestRepository, driverRepository)
DI_TOKENS.GetRaceProtestsUseCase,
new GetRaceProtestsUseCase(protestRepository, driverRepository, raceProtestsPresenter)
);
const racePenaltiesPresenter = new RacePenaltiesPresenter();
container.registerInstance(
DI_TOKENS.GetRacePenaltiesQuery,
new GetRacePenaltiesQuery(penaltyRepository, driverRepository)
DI_TOKENS.GetRacePenaltiesUseCase,
new GetRacePenaltiesUseCase(penaltyRepository, driverRepository, racePenaltiesPresenter)
);
// Register queries - Notifications
@@ -990,31 +1110,35 @@ export function configureDIContainer(): void {
new GetUnreadNotificationsQuery(notificationRepository)
);
// Register queries - Sponsors
// Register use cases - Sponsors
const sponsorRepository = container.resolve<ISponsorRepository>(DI_TOKENS.SponsorRepository);
const seasonSponsorshipRepository = container.resolve<ISeasonSponsorshipRepository>(DI_TOKENS.SeasonSponsorshipRepository);
const sponsorDashboardPresenter = new SponsorDashboardPresenter();
container.registerInstance(
DI_TOKENS.GetSponsorDashboardQuery,
new GetSponsorDashboardQuery(
DI_TOKENS.GetSponsorDashboardUseCase,
new GetSponsorDashboardUseCase(
sponsorRepository,
seasonSponsorshipRepository,
seasonRepository,
leagueRepository,
leagueMembershipRepository,
raceRepository
raceRepository,
sponsorDashboardPresenter
)
);
const sponsorSponsorshipsPresenter = new SponsorSponsorshipsPresenter();
container.registerInstance(
DI_TOKENS.GetSponsorSponsorshipsQuery,
new GetSponsorSponsorshipsQuery(
DI_TOKENS.GetSponsorSponsorshipsUseCase,
new GetSponsorSponsorshipsUseCase(
sponsorRepository,
seasonSponsorshipRepository,
seasonRepository,
leagueRepository,
leagueMembershipRepository,
raceRepository
raceRepository,
sponsorSponsorshipsPresenter
)
);
@@ -1022,20 +1146,24 @@ export function configureDIContainer(): void {
const sponsorshipRequestRepository = container.resolve<ISponsorshipRequestRepository>(DI_TOKENS.SponsorshipRequestRepository);
const sponsorshipPricingRepository = container.resolve<ISponsorshipPricingRepository>(DI_TOKENS.SponsorshipPricingRepository);
const pendingSponsorshipRequestsPresenter = new PendingSponsorshipRequestsPresenter();
container.registerInstance(
DI_TOKENS.GetPendingSponsorshipRequestsQuery,
new GetPendingSponsorshipRequestsQuery(
DI_TOKENS.GetPendingSponsorshipRequestsUseCase,
new GetPendingSponsorshipRequestsUseCase(
sponsorshipRequestRepository,
sponsorRepository
sponsorRepository,
pendingSponsorshipRequestsPresenter
)
);
const entitySponsorshipPricingPresenter = new EntitySponsorshipPricingPresenter();
container.registerInstance(
DI_TOKENS.GetEntitySponsorshipPricingQuery,
new GetEntitySponsorshipPricingQuery(
DI_TOKENS.GetEntitySponsorshipPricingUseCase,
new GetEntitySponsorshipPricingUseCase(
sponsorshipPricingRepository,
sponsorshipRequestRepository,
seasonSponsorshipRepository
seasonSponsorshipRepository,
entitySponsorshipPricingPresenter
)
);

View File

@@ -41,51 +41,54 @@ import type {
JoinLeagueUseCase,
RegisterForRaceUseCase,
WithdrawFromRaceUseCase,
IsDriverRegisteredForRaceQuery,
GetRaceRegistrationsQuery,
CreateTeamUseCase,
JoinTeamUseCase,
LeaveTeamUseCase,
ApproveTeamJoinRequestUseCase,
RejectTeamJoinRequestUseCase,
UpdateTeamUseCase,
GetAllTeamsQuery,
GetTeamDetailsQuery,
GetTeamMembersQuery,
GetTeamJoinRequestsQuery,
GetDriverTeamQuery,
GetLeagueStandingsQuery,
GetLeagueDriverSeasonStatsQuery,
GetAllLeaguesWithCapacityQuery,
GetAllLeaguesWithCapacityAndScoringQuery,
ListLeagueScoringPresetsQuery,
GetLeagueScoringConfigQuery,
GetAllTeamsUseCase,
GetTeamDetailsUseCase,
GetTeamMembersUseCase,
GetTeamJoinRequestsUseCase,
GetDriverTeamUseCase,
CreateLeagueWithSeasonAndScoringUseCase,
GetLeagueFullConfigQuery,
GetRaceWithSOFQuery,
GetLeagueStatsQuery,
FileProtestUseCase,
ReviewProtestUseCase,
ApplyPenaltyUseCase,
GetRaceProtestsQuery,
GetRacePenaltiesQuery,
RequestProtestDefenseUseCase,
SubmitProtestDefenseUseCase,
GetSponsorDashboardQuery,
GetSponsorSponsorshipsQuery,
GetSponsorDashboardUseCase,
GetSponsorSponsorshipsUseCase,
ApplyForSponsorshipUseCase,
AcceptSponsorshipRequestUseCase,
RejectSponsorshipRequestUseCase,
GetPendingSponsorshipRequestsQuery,
GetEntitySponsorshipPricingQuery,
GetPendingSponsorshipRequestsUseCase,
GetEntitySponsorshipPricingUseCase,
} from '@gridpilot/racing/application';
import type { IsDriverRegisteredForRaceUseCase } from '@gridpilot/racing/application/use-cases/IsDriverRegisteredForRaceQuery';
import type { GetRaceRegistrationsUseCase } from '@gridpilot/racing/application/use-cases/GetRaceRegistrationsQuery';
import type { GetRaceWithSOFUseCase } from '@gridpilot/racing/application/use-cases/GetRaceWithSOFQuery';
import type { GetRaceProtestsUseCase } from '@gridpilot/racing/application/use-cases/GetRaceProtestsQuery';
import type { GetRacePenaltiesUseCase } from '@gridpilot/racing/application/use-cases/GetRacePenaltiesQuery';
import type { GetRacesPageDataUseCase } from '@gridpilot/racing/application/use-cases/GetRacesPageDataUseCase';
import type { GetDriversLeaderboardUseCase } from '@gridpilot/racing/application/use-cases/GetDriversLeaderboardUseCase';
import type { GetTeamsLeaderboardUseCase } from '@gridpilot/racing/application/use-cases/GetTeamsLeaderboardUseCase';
import type { GetLeagueStandingsUseCase } from '@gridpilot/racing/application/use-cases/GetLeagueStandingsQuery';
import type { GetLeagueDriverSeasonStatsUseCase } from '@gridpilot/racing/application/use-cases/GetLeagueDriverSeasonStatsQuery';
import type { GetAllLeaguesWithCapacityUseCase } from '@gridpilot/racing/application/use-cases/GetAllLeaguesWithCapacityQuery';
import type { GetAllLeaguesWithCapacityAndScoringUseCase } from '@gridpilot/racing/application/use-cases/GetAllLeaguesWithCapacityAndScoringQuery';
import type { ListLeagueScoringPresetsUseCase } from '@gridpilot/racing/application/use-cases/ListLeagueScoringPresetsQuery';
import type { GetLeagueScoringConfigUseCase } from '@gridpilot/racing/application/use-cases/GetLeagueScoringConfigQuery';
import type { GetLeagueFullConfigUseCase } from '@gridpilot/racing/application/use-cases/GetLeagueFullConfigQuery';
import type { GetLeagueStatsUseCase } from '@gridpilot/racing/application/use-cases/GetLeagueStatsQuery';
import type { ISponsorRepository } from '@gridpilot/racing/domain/repositories/ISponsorRepository';
import type { ISeasonSponsorshipRepository } from '@gridpilot/racing/domain/repositories/ISeasonSponsorshipRepository';
import type { ISponsorshipRequestRepository } from '@gridpilot/racing/domain/repositories/ISponsorshipRequestRepository';
import type { ISponsorshipPricingRepository } from '@gridpilot/racing/domain/repositories/ISponsorshipPricingRepository';
import type { TransferLeagueOwnershipUseCase } from '@gridpilot/racing/application/use-cases/TransferLeagueOwnershipUseCase';
import type { DriverRatingProvider } from '@gridpilot/racing/application';
import type { PreviewLeagueScheduleQuery } from '@gridpilot/racing/application';
import type { PreviewLeagueScheduleUseCase } from '@gridpilot/racing/application';
import type { LeagueScoringPresetProvider } from '@gridpilot/racing/application/ports/LeagueScoringPresetProvider';
import { createDemoDriverStats, getDemoLeagueRankings, type DriverStats } from '@gridpilot/testing-support';
@@ -211,64 +214,79 @@ class DIContainer {
return getDIContainer().resolve<WithdrawFromRaceUseCase>(DI_TOKENS.WithdrawFromRaceUseCase);
}
get isDriverRegisteredForRaceQuery(): IsDriverRegisteredForRaceQuery {
get isDriverRegisteredForRaceUseCase(): IsDriverRegisteredForRaceUseCase {
this.ensureInitialized();
return getDIContainer().resolve<IsDriverRegisteredForRaceQuery>(DI_TOKENS.IsDriverRegisteredForRaceQuery);
return getDIContainer().resolve<IsDriverRegisteredForRaceUseCase>(DI_TOKENS.IsDriverRegisteredForRaceUseCase);
}
get getRaceRegistrationsQuery(): GetRaceRegistrationsQuery {
get getRaceRegistrationsUseCase(): GetRaceRegistrationsUseCase {
this.ensureInitialized();
return getDIContainer().resolve<GetRaceRegistrationsQuery>(DI_TOKENS.GetRaceRegistrationsQuery);
return getDIContainer().resolve<GetRaceRegistrationsUseCase>(DI_TOKENS.GetRaceRegistrationsUseCase);
}
get getLeagueStandingsQuery(): GetLeagueStandingsQuery {
get getLeagueStandingsUseCase(): GetLeagueStandingsUseCase {
this.ensureInitialized();
return getDIContainer().resolve<GetLeagueStandingsQuery>(DI_TOKENS.GetLeagueStandingsQuery);
return getDIContainer().resolve<GetLeagueStandingsUseCase>(DI_TOKENS.GetLeagueStandingsUseCase);
}
get getLeagueDriverSeasonStatsQuery(): GetLeagueDriverSeasonStatsQuery {
get getLeagueDriverSeasonStatsUseCase(): GetLeagueDriverSeasonStatsUseCase {
this.ensureInitialized();
return getDIContainer().resolve<GetLeagueDriverSeasonStatsQuery>(DI_TOKENS.GetLeagueDriverSeasonStatsQuery);
return getDIContainer().resolve<GetLeagueDriverSeasonStatsUseCase>(DI_TOKENS.GetLeagueDriverSeasonStatsUseCase);
}
get getAllLeaguesWithCapacityQuery(): GetAllLeaguesWithCapacityQuery {
get getAllLeaguesWithCapacityUseCase(): GetAllLeaguesWithCapacityUseCase {
this.ensureInitialized();
return getDIContainer().resolve<GetAllLeaguesWithCapacityQuery>(DI_TOKENS.GetAllLeaguesWithCapacityQuery);
return getDIContainer().resolve<GetAllLeaguesWithCapacityUseCase>(DI_TOKENS.GetAllLeaguesWithCapacityUseCase);
}
get getAllLeaguesWithCapacityAndScoringQuery(): GetAllLeaguesWithCapacityAndScoringQuery {
get getAllLeaguesWithCapacityAndScoringUseCase(): GetAllLeaguesWithCapacityAndScoringUseCase {
this.ensureInitialized();
return getDIContainer().resolve<GetAllLeaguesWithCapacityAndScoringQuery>(DI_TOKENS.GetAllLeaguesWithCapacityAndScoringQuery);
return getDIContainer().resolve<GetAllLeaguesWithCapacityAndScoringUseCase>(DI_TOKENS.GetAllLeaguesWithCapacityAndScoringUseCase);
}
get listLeagueScoringPresetsQuery(): ListLeagueScoringPresetsQuery {
get listLeagueScoringPresetsUseCase(): ListLeagueScoringPresetsUseCase {
this.ensureInitialized();
return getDIContainer().resolve<ListLeagueScoringPresetsQuery>(DI_TOKENS.ListLeagueScoringPresetsQuery);
return getDIContainer().resolve<ListLeagueScoringPresetsUseCase>(DI_TOKENS.ListLeagueScoringPresetsUseCase);
}
get getLeagueScoringConfigQuery(): GetLeagueScoringConfigQuery {
get getLeagueScoringConfigUseCase(): GetLeagueScoringConfigUseCase {
this.ensureInitialized();
return getDIContainer().resolve<GetLeagueScoringConfigQuery>(DI_TOKENS.GetLeagueScoringConfigQuery);
return getDIContainer().resolve<GetLeagueScoringConfigUseCase>(DI_TOKENS.GetLeagueScoringConfigUseCase);
}
get getLeagueFullConfigQuery(): GetLeagueFullConfigQuery {
get getLeagueFullConfigUseCase(): GetLeagueFullConfigUseCase {
this.ensureInitialized();
return getDIContainer().resolve<GetLeagueFullConfigQuery>(DI_TOKENS.GetLeagueFullConfigQuery);
return getDIContainer().resolve<GetLeagueFullConfigUseCase>(DI_TOKENS.GetLeagueFullConfigUseCase);
}
get previewLeagueScheduleQuery(): PreviewLeagueScheduleQuery {
get previewLeagueScheduleUseCase(): PreviewLeagueScheduleUseCase {
this.ensureInitialized();
return getDIContainer().resolve<PreviewLeagueScheduleQuery>(DI_TOKENS.PreviewLeagueScheduleQuery);
return getDIContainer().resolve<PreviewLeagueScheduleUseCase>(DI_TOKENS.PreviewLeagueScheduleUseCase);
}
get getRaceWithSOFQuery(): GetRaceWithSOFQuery {
get getRaceWithSOFUseCase(): GetRaceWithSOFUseCase {
this.ensureInitialized();
return getDIContainer().resolve<GetRaceWithSOFQuery>(DI_TOKENS.GetRaceWithSOFQuery);
return getDIContainer().resolve<GetRaceWithSOFUseCase>(DI_TOKENS.GetRaceWithSOFUseCase);
}
get getLeagueStatsQuery(): GetLeagueStatsQuery {
get getLeagueStatsUseCase(): GetLeagueStatsUseCase {
this.ensureInitialized();
return getDIContainer().resolve<GetLeagueStatsQuery>(DI_TOKENS.GetLeagueStatsQuery);
return getDIContainer().resolve<GetLeagueStatsUseCase>(DI_TOKENS.GetLeagueStatsUseCase);
}
get getRacesPageDataUseCase(): GetRacesPageDataUseCase {
this.ensureInitialized();
return getDIContainer().resolve<GetRacesPageDataUseCase>(DI_TOKENS.GetRacesPageDataUseCase);
}
get getDriversLeaderboardUseCase(): GetDriversLeaderboardUseCase {
this.ensureInitialized();
return getDIContainer().resolve<GetDriversLeaderboardUseCase>(DI_TOKENS.GetDriversLeaderboardUseCase);
}
get getTeamsLeaderboardUseCase(): GetTeamsLeaderboardUseCase {
this.ensureInitialized();
return getDIContainer().resolve<GetTeamsLeaderboardUseCase>(DI_TOKENS.GetTeamsLeaderboardUseCase);
}
get driverRatingProvider(): DriverRatingProvider {
@@ -311,29 +329,29 @@ class DIContainer {
return getDIContainer().resolve<UpdateTeamUseCase>(DI_TOKENS.UpdateTeamUseCase);
}
get getAllTeamsQuery(): GetAllTeamsQuery {
get getAllTeamsUseCase(): GetAllTeamsUseCase {
this.ensureInitialized();
return getDIContainer().resolve<GetAllTeamsQuery>(DI_TOKENS.GetAllTeamsQuery);
return getDIContainer().resolve<GetAllTeamsUseCase>(DI_TOKENS.GetAllTeamsUseCase);
}
get getTeamDetailsQuery(): GetTeamDetailsQuery {
get getTeamDetailsUseCase(): GetTeamDetailsUseCase {
this.ensureInitialized();
return getDIContainer().resolve<GetTeamDetailsQuery>(DI_TOKENS.GetTeamDetailsQuery);
return getDIContainer().resolve<GetTeamDetailsUseCase>(DI_TOKENS.GetTeamDetailsUseCase);
}
get getTeamMembersQuery(): GetTeamMembersQuery {
get getTeamMembersUseCase(): GetTeamMembersUseCase {
this.ensureInitialized();
return getDIContainer().resolve<GetTeamMembersQuery>(DI_TOKENS.GetTeamMembersQuery);
return getDIContainer().resolve<GetTeamMembersUseCase>(DI_TOKENS.GetTeamMembersUseCase);
}
get getTeamJoinRequestsQuery(): GetTeamJoinRequestsQuery {
get getTeamJoinRequestsUseCase(): GetTeamJoinRequestsUseCase {
this.ensureInitialized();
return getDIContainer().resolve<GetTeamJoinRequestsQuery>(DI_TOKENS.GetTeamJoinRequestsQuery);
return getDIContainer().resolve<GetTeamJoinRequestsUseCase>(DI_TOKENS.GetTeamJoinRequestsUseCase);
}
get getDriverTeamQuery(): GetDriverTeamQuery {
get getDriverTeamUseCase(): GetDriverTeamUseCase {
this.ensureInitialized();
return getDIContainer().resolve<GetDriverTeamQuery>(DI_TOKENS.GetDriverTeamQuery);
return getDIContainer().resolve<GetDriverTeamUseCase>(DI_TOKENS.GetDriverTeamUseCase);
}
get teamRepository(): ITeamRepository {
@@ -411,14 +429,14 @@ class DIContainer {
return getDIContainer().resolve<ApplyPenaltyUseCase>(DI_TOKENS.ApplyPenaltyUseCase);
}
get getRaceProtestsQuery(): GetRaceProtestsQuery {
get getRaceProtestsUseCase(): GetRaceProtestsUseCase {
this.ensureInitialized();
return getDIContainer().resolve<GetRaceProtestsQuery>(DI_TOKENS.GetRaceProtestsQuery);
return getDIContainer().resolve<GetRaceProtestsUseCase>(DI_TOKENS.GetRaceProtestsUseCase);
}
get getRacePenaltiesQuery(): GetRacePenaltiesQuery {
get getRacePenaltiesUseCase(): GetRacePenaltiesUseCase {
this.ensureInitialized();
return getDIContainer().resolve<GetRacePenaltiesQuery>(DI_TOKENS.GetRacePenaltiesQuery);
return getDIContainer().resolve<GetRacePenaltiesUseCase>(DI_TOKENS.GetRacePenaltiesUseCase);
}
get requestProtestDefenseUseCase(): RequestProtestDefenseUseCase {
@@ -446,14 +464,14 @@ class DIContainer {
return getDIContainer().resolve<ISeasonSponsorshipRepository>(DI_TOKENS.SeasonSponsorshipRepository);
}
get getSponsorDashboardQuery(): GetSponsorDashboardQuery {
get getSponsorDashboardUseCase(): GetSponsorDashboardUseCase {
this.ensureInitialized();
return getDIContainer().resolve<GetSponsorDashboardQuery>(DI_TOKENS.GetSponsorDashboardQuery);
return getDIContainer().resolve<GetSponsorDashboardUseCase>(DI_TOKENS.GetSponsorDashboardUseCase);
}
get getSponsorSponsorshipsQuery(): GetSponsorSponsorshipsQuery {
get getSponsorSponsorshipsUseCase(): GetSponsorSponsorshipsUseCase {
this.ensureInitialized();
return getDIContainer().resolve<GetSponsorSponsorshipsQuery>(DI_TOKENS.GetSponsorSponsorshipsQuery);
return getDIContainer().resolve<GetSponsorSponsorshipsUseCase>(DI_TOKENS.GetSponsorSponsorshipsUseCase);
}
get sponsorshipRequestRepository(): ISponsorshipRequestRepository {
@@ -481,14 +499,14 @@ class DIContainer {
return getDIContainer().resolve<RejectSponsorshipRequestUseCase>(DI_TOKENS.RejectSponsorshipRequestUseCase);
}
get getPendingSponsorshipRequestsQuery(): GetPendingSponsorshipRequestsQuery {
get getPendingSponsorshipRequestsUseCase(): GetPendingSponsorshipRequestsUseCase {
this.ensureInitialized();
return getDIContainer().resolve<GetPendingSponsorshipRequestsQuery>(DI_TOKENS.GetPendingSponsorshipRequestsQuery);
return getDIContainer().resolve<GetPendingSponsorshipRequestsUseCase>(DI_TOKENS.GetPendingSponsorshipRequestsUseCase);
}
get getEntitySponsorshipPricingQuery(): GetEntitySponsorshipPricingQuery {
get getEntitySponsorshipPricingUseCase(): GetEntitySponsorshipPricingUseCase {
this.ensureInitialized();
return getDIContainer().resolve<GetEntitySponsorshipPricingQuery>(DI_TOKENS.GetEntitySponsorshipPricingQuery);
return getDIContainer().resolve<GetEntitySponsorshipPricingUseCase>(DI_TOKENS.GetEntitySponsorshipPricingUseCase);
}
}
@@ -543,57 +561,68 @@ export function getWithdrawFromRaceUseCase(): WithdrawFromRaceUseCase {
return DIContainer.getInstance().withdrawFromRaceUseCase;
}
export function getIsDriverRegisteredForRaceQuery(): IsDriverRegisteredForRaceQuery {
return DIContainer.getInstance().isDriverRegisteredForRaceQuery;
export function getIsDriverRegisteredForRaceUseCase(): IsDriverRegisteredForRaceUseCase {
return DIContainer.getInstance().isDriverRegisteredForRaceUseCase;
}
export function getGetRaceRegistrationsQuery(): GetRaceRegistrationsQuery {
return DIContainer.getInstance().getRaceRegistrationsQuery;
export function getGetRaceRegistrationsUseCase(): GetRaceRegistrationsUseCase {
return DIContainer.getInstance().getRaceRegistrationsUseCase;
}
export function getGetLeagueStandingsQuery(): GetLeagueStandingsQuery {
return DIContainer.getInstance().getLeagueStandingsQuery;
export function getGetLeagueStandingsUseCase(): GetLeagueStandingsUseCase {
return DIContainer.getInstance().getLeagueStandingsUseCase;
}
export function getGetLeagueDriverSeasonStatsQuery(): GetLeagueDriverSeasonStatsQuery {
return DIContainer.getInstance().getLeagueDriverSeasonStatsQuery;
export function getGetLeagueDriverSeasonStatsUseCase(): GetLeagueDriverSeasonStatsUseCase {
return DIContainer.getInstance().getLeagueDriverSeasonStatsUseCase;
}
export function getGetAllLeaguesWithCapacityQuery(): GetAllLeaguesWithCapacityQuery {
return DIContainer.getInstance().getAllLeaguesWithCapacityQuery;
export function getGetAllLeaguesWithCapacityUseCase(): GetAllLeaguesWithCapacityUseCase {
return DIContainer.getInstance().getAllLeaguesWithCapacityUseCase;
}
export function getGetAllLeaguesWithCapacityAndScoringQuery(): GetAllLeaguesWithCapacityAndScoringQuery {
return DIContainer.getInstance().getAllLeaguesWithCapacityAndScoringQuery;
export function getGetAllLeaguesWithCapacityAndScoringUseCase(): GetAllLeaguesWithCapacityAndScoringUseCase {
return DIContainer.getInstance().getAllLeaguesWithCapacityAndScoringUseCase;
}
export function getGetLeagueScoringConfigQuery(): GetLeagueScoringConfigQuery {
return DIContainer.getInstance().getLeagueScoringConfigQuery;
export function getGetLeagueScoringConfigUseCase(): GetLeagueScoringConfigUseCase {
return DIContainer.getInstance().getLeagueScoringConfigUseCase;
}
export function getGetLeagueFullConfigQuery(): GetLeagueFullConfigQuery {
return DIContainer.getInstance().getLeagueFullConfigQuery;
export function getGetLeagueFullConfigUseCase(): GetLeagueFullConfigUseCase {
return DIContainer.getInstance().getLeagueFullConfigUseCase;
}
// Placeholder export for future schedule preview API wiring.
export function getPreviewLeagueScheduleQuery(): PreviewLeagueScheduleQuery {
return DIContainer.getInstance().previewLeagueScheduleQuery;
export function getPreviewLeagueScheduleUseCase(): PreviewLeagueScheduleUseCase {
return DIContainer.getInstance().previewLeagueScheduleUseCase;
}
export function getListLeagueScoringPresetsQuery(): ListLeagueScoringPresetsQuery {
return DIContainer.getInstance().listLeagueScoringPresetsQuery;
export function getListLeagueScoringPresetsUseCase(): ListLeagueScoringPresetsUseCase {
return DIContainer.getInstance().listLeagueScoringPresetsUseCase;
}
export function getCreateLeagueWithSeasonAndScoringUseCase(): CreateLeagueWithSeasonAndScoringUseCase {
return DIContainer.getInstance().createLeagueWithSeasonAndScoringUseCase;
}
export function getGetRaceWithSOFQuery(): GetRaceWithSOFQuery {
return DIContainer.getInstance().getRaceWithSOFQuery;
export function getGetRaceWithSOFUseCase(): GetRaceWithSOFUseCase {
return DIContainer.getInstance().getRaceWithSOFUseCase;
}
export function getGetLeagueStatsQuery(): GetLeagueStatsQuery {
return DIContainer.getInstance().getLeagueStatsQuery;
export function getGetLeagueStatsUseCase(): GetLeagueStatsUseCase {
return DIContainer.getInstance().getLeagueStatsUseCase;
}
export function getGetRacesPageDataUseCase(): GetRacesPageDataUseCase {
return DIContainer.getInstance().getRacesPageDataUseCase;
}
export function getGetDriversLeaderboardUseCase(): GetDriversLeaderboardUseCase {
return DIContainer.getInstance().getDriversLeaderboardUseCase;
}
export function getGetTeamsLeaderboardUseCase(): GetTeamsLeaderboardUseCase {
return DIContainer.getInstance().getTeamsLeaderboardUseCase;
}
export function getDriverRatingProvider(): DriverRatingProvider {
@@ -632,24 +661,24 @@ export function getUpdateTeamUseCase(): UpdateTeamUseCase {
return DIContainer.getInstance().updateTeamUseCase;
}
export function getGetAllTeamsQuery(): GetAllTeamsQuery {
return DIContainer.getInstance().getAllTeamsQuery;
export function getGetAllTeamsUseCase(): GetAllTeamsUseCase {
return DIContainer.getInstance().getAllTeamsUseCase;
}
export function getGetTeamDetailsQuery(): GetTeamDetailsQuery {
return DIContainer.getInstance().getTeamDetailsQuery;
export function getGetTeamDetailsUseCase(): GetTeamDetailsUseCase {
return DIContainer.getInstance().getTeamDetailsUseCase;
}
export function getGetTeamMembersQuery(): GetTeamMembersQuery {
return DIContainer.getInstance().getTeamMembersQuery;
export function getGetTeamMembersUseCase(): GetTeamMembersUseCase {
return DIContainer.getInstance().getTeamMembersUseCase;
}
export function getGetTeamJoinRequestsQuery(): GetTeamJoinRequestsQuery {
return DIContainer.getInstance().getTeamJoinRequestsQuery;
export function getGetTeamJoinRequestsUseCase(): GetTeamJoinRequestsUseCase {
return DIContainer.getInstance().getTeamJoinRequestsUseCase;
}
export function getGetDriverTeamQuery(): GetDriverTeamQuery {
return DIContainer.getInstance().getDriverTeamQuery;
export function getGetDriverTeamUseCase(): GetDriverTeamUseCase {
return DIContainer.getInstance().getDriverTeamUseCase;
}
export function getFeedRepository(): IFeedRepository {
@@ -708,12 +737,12 @@ export function getApplyPenaltyUseCase(): ApplyPenaltyUseCase {
return DIContainer.getInstance().applyPenaltyUseCase;
}
export function getGetRaceProtestsQuery(): GetRaceProtestsQuery {
return DIContainer.getInstance().getRaceProtestsQuery;
export function getGetRaceProtestsUseCase(): GetRaceProtestsUseCase {
return DIContainer.getInstance().getRaceProtestsUseCase;
}
export function getGetRacePenaltiesQuery(): GetRacePenaltiesQuery {
return DIContainer.getInstance().getRacePenaltiesQuery;
export function getGetRacePenaltiesUseCase(): GetRacePenaltiesUseCase {
return DIContainer.getInstance().getRacePenaltiesUseCase;
}
export function getRequestProtestDefenseUseCase(): RequestProtestDefenseUseCase {
@@ -736,12 +765,12 @@ export function getSeasonSponsorshipRepository(): ISeasonSponsorshipRepository {
return DIContainer.getInstance().seasonSponsorshipRepository;
}
export function getGetSponsorDashboardQuery(): GetSponsorDashboardQuery {
return DIContainer.getInstance().getSponsorDashboardQuery;
export function getGetSponsorDashboardUseCase(): GetSponsorDashboardUseCase {
return DIContainer.getInstance().getSponsorDashboardUseCase;
}
export function getGetSponsorSponsorshipsQuery(): GetSponsorSponsorshipsQuery {
return DIContainer.getInstance().getSponsorSponsorshipsQuery;
export function getGetSponsorSponsorshipsUseCase(): GetSponsorSponsorshipsUseCase {
return DIContainer.getInstance().getSponsorSponsorshipsUseCase;
}
export function getSponsorshipRequestRepository(): ISponsorshipRequestRepository {
@@ -764,12 +793,12 @@ export function getRejectSponsorshipRequestUseCase(): RejectSponsorshipRequestUs
return DIContainer.getInstance().rejectSponsorshipRequestUseCase;
}
export function getGetPendingSponsorshipRequestsQuery(): GetPendingSponsorshipRequestsQuery {
return DIContainer.getInstance().getPendingSponsorshipRequestsQuery;
export function getGetPendingSponsorshipRequestsUseCase(): GetPendingSponsorshipRequestsUseCase {
return DIContainer.getInstance().getPendingSponsorshipRequestsUseCase;
}
export function getGetEntitySponsorshipPricingQuery(): GetEntitySponsorshipPricingQuery {
return DIContainer.getInstance().getEntitySponsorshipPricingQuery;
export function getGetEntitySponsorshipPricingUseCase(): GetEntitySponsorshipPricingUseCase {
return DIContainer.getInstance().getEntitySponsorshipPricingUseCase;
}
/**

View File

@@ -64,38 +64,41 @@ export const DI_TOKENS = {
MarkNotificationReadUseCase: Symbol.for('MarkNotificationReadUseCase'),
// Queries - Racing
IsDriverRegisteredForRaceQuery: Symbol.for('IsDriverRegisteredForRaceQuery'),
GetRaceRegistrationsQuery: Symbol.for('GetRaceRegistrationsQuery'),
GetLeagueStandingsQuery: Symbol.for('GetLeagueStandingsQuery'),
GetLeagueDriverSeasonStatsQuery: Symbol.for('GetLeagueDriverSeasonStatsQuery'),
GetAllLeaguesWithCapacityQuery: Symbol.for('GetAllLeaguesWithCapacityQuery'),
GetAllLeaguesWithCapacityAndScoringQuery: Symbol.for('GetAllLeaguesWithCapacityAndScoringQuery'),
ListLeagueScoringPresetsQuery: Symbol.for('ListLeagueScoringPresetsQuery'),
GetLeagueScoringConfigQuery: Symbol.for('GetLeagueScoringConfigQuery'),
GetLeagueFullConfigQuery: Symbol.for('GetLeagueFullConfigQuery'),
PreviewLeagueScheduleQuery: Symbol.for('PreviewLeagueScheduleQuery'),
GetRaceWithSOFQuery: Symbol.for('GetRaceWithSOFQuery'),
GetLeagueStatsQuery: Symbol.for('GetLeagueStatsQuery'),
IsDriverRegisteredForRaceUseCase: Symbol.for('IsDriverRegisteredForRaceUseCase'),
GetRaceRegistrationsUseCase: Symbol.for('GetRaceRegistrationsUseCase'),
GetLeagueStandingsUseCase: Symbol.for('GetLeagueStandingsUseCase'),
GetLeagueDriverSeasonStatsUseCase: Symbol.for('GetLeagueDriverSeasonStatsUseCase'),
GetAllLeaguesWithCapacityUseCase: Symbol.for('GetAllLeaguesWithCapacityUseCase'),
GetAllLeaguesWithCapacityAndScoringUseCase: Symbol.for('GetAllLeaguesWithCapacityAndScoringUseCase'),
ListLeagueScoringPresetsUseCase: Symbol.for('ListLeagueScoringPresetsUseCase'),
GetLeagueScoringConfigUseCase: Symbol.for('GetLeagueScoringConfigUseCase'),
GetLeagueFullConfigUseCase: Symbol.for('GetLeagueFullConfigUseCase'),
PreviewLeagueScheduleUseCase: Symbol.for('PreviewLeagueScheduleUseCase'),
GetRaceWithSOFUseCase: Symbol.for('GetRaceWithSOFUseCase'),
GetLeagueStatsUseCase: Symbol.for('GetLeagueStatsUseCase'),
GetRacesPageDataUseCase: Symbol.for('GetRacesPageDataUseCase'),
GetDriversLeaderboardUseCase: Symbol.for('GetDriversLeaderboardUseCase'),
GetTeamsLeaderboardUseCase: Symbol.for('GetTeamsLeaderboardUseCase'),
// Queries - Teams
GetAllTeamsQuery: Symbol.for('GetAllTeamsQuery'),
GetTeamDetailsQuery: Symbol.for('GetTeamDetailsQuery'),
GetTeamMembersQuery: Symbol.for('GetTeamMembersQuery'),
GetTeamJoinRequestsQuery: Symbol.for('GetTeamJoinRequestsQuery'),
GetDriverTeamQuery: Symbol.for('GetDriverTeamQuery'),
// Use Cases - Teams (Query-like)
GetAllTeamsUseCase: Symbol.for('GetAllTeamsUseCase'),
GetTeamDetailsUseCase: Symbol.for('GetTeamDetailsUseCase'),
GetTeamMembersUseCase: Symbol.for('GetTeamMembersUseCase'),
GetTeamJoinRequestsUseCase: Symbol.for('GetTeamJoinRequestsUseCase'),
GetDriverTeamUseCase: Symbol.for('GetDriverTeamUseCase'),
// Queries - Stewarding
GetRaceProtestsQuery: Symbol.for('GetRaceProtestsQuery'),
GetRacePenaltiesQuery: Symbol.for('GetRacePenaltiesQuery'),
GetRaceProtestsUseCase: Symbol.for('GetRaceProtestsUseCase'),
GetRacePenaltiesUseCase: Symbol.for('GetRacePenaltiesUseCase'),
// Queries - Notifications
GetUnreadNotificationsQuery: Symbol.for('GetUnreadNotificationsQuery'),
// Queries - Sponsors
GetSponsorDashboardQuery: Symbol.for('GetSponsorDashboardQuery'),
GetSponsorSponsorshipsQuery: Symbol.for('GetSponsorSponsorshipsQuery'),
GetPendingSponsorshipRequestsQuery: Symbol.for('GetPendingSponsorshipRequestsQuery'),
GetEntitySponsorshipPricingQuery: Symbol.for('GetEntitySponsorshipPricingQuery'),
// Use Cases - Sponsors
GetSponsorDashboardUseCase: Symbol.for('GetSponsorDashboardUseCase'),
GetSponsorSponsorshipsUseCase: Symbol.for('GetSponsorSponsorshipsUseCase'),
GetPendingSponsorshipRequestsUseCase: Symbol.for('GetPendingSponsorshipRequestsUseCase'),
GetEntitySponsorshipPricingUseCase: Symbol.for('GetEntitySponsorshipPricingUseCase'),
// Use Cases - Sponsorship
ApplyForSponsorshipUseCase: Symbol.for('ApplyForSponsorshipUseCase'),
@@ -104,6 +107,20 @@ export const DI_TOKENS = {
// Data
DriverStats: Symbol.for('DriverStats'),
// Presenters - Racing
RaceWithSOFPresenter: Symbol.for('IRaceWithSOFPresenter'),
RaceProtestsPresenter: Symbol.for('IRaceProtestsPresenter'),
RacePenaltiesPresenter: Symbol.for('IRacePenaltiesPresenter'),
RaceRegistrationsPresenter: Symbol.for('IRaceRegistrationsPresenter'),
DriverRegistrationStatusPresenter: Symbol.for('IDriverRegistrationStatusPresenter'),
// Presenters - Sponsors
SponsorDashboardPresenter: Symbol.for('ISponsorDashboardPresenter'),
SponsorSponsorshipsPresenter: Symbol.for('ISponsorSponsorshipsPresenter'),
PendingSponsorshipRequestsPresenter: Symbol.for('IPendingSponsorshipRequestsPresenter'),
EntitySponsorshipPricingPresenter: Symbol.for('IEntitySponsorshipPricingPresenter'),
LeagueSchedulePreviewPresenter: Symbol.for('ILeagueSchedulePreviewPresenter'),
} as const;
export type DITokens = typeof DI_TOKENS;

View File

@@ -0,0 +1,112 @@
import type { League } from '@gridpilot/racing/domain/entities/League';
import type {
IAllLeaguesWithCapacityAndScoringPresenter,
LeagueEnrichedData,
LeagueSummaryViewModel,
AllLeaguesWithCapacityAndScoringViewModel,
} from '@gridpilot/racing/application/presenters/IAllLeaguesWithCapacityAndScoringPresenter';
export class AllLeaguesWithCapacityAndScoringPresenter implements IAllLeaguesWithCapacityAndScoringPresenter {
private viewModel: AllLeaguesWithCapacityAndScoringViewModel | null = null;
present(enrichedLeagues: LeagueEnrichedData[]): AllLeaguesWithCapacityAndScoringViewModel {
const leagueItems: LeagueSummaryViewModel[] = enrichedLeagues.map((data) => {
const { league, usedDriverSlots, season, scoringConfig, game, preset } = data;
const configuredMaxDrivers = league.settings.maxDrivers ?? usedDriverSlots;
const safeMaxDrivers = Math.max(configuredMaxDrivers, usedDriverSlots);
const structureSummary = `Solo • ${safeMaxDrivers} drivers`;
const qualifyingMinutes = 30;
const mainRaceMinutes =
typeof league.settings.sessionDuration === 'number'
? league.settings.sessionDuration
: 40;
const timingSummary = `${qualifyingMinutes} min Quali • ${mainRaceMinutes} min Race`;
let scoringSummary: LeagueSummaryViewModel['scoring'] | undefined;
let scoringPatternSummary: string | undefined;
if (season && scoringConfig && game) {
const dropPolicySummary =
preset?.dropPolicySummary ?? this.deriveDropPolicySummary(scoringConfig);
const primaryChampionshipType =
preset?.primaryChampionshipType ??
(scoringConfig.championships[0]?.type ?? 'driver');
const scoringPresetName = preset?.name ?? 'Custom';
scoringPatternSummary = `${scoringPresetName}${dropPolicySummary}`;
scoringSummary = {
gameId: game.id,
gameName: game.name,
primaryChampionshipType,
scoringPresetId: scoringConfig.scoringPresetId ?? 'custom',
scoringPresetName,
dropPolicySummary,
scoringPatternSummary,
};
}
return {
id: league.id,
name: league.name,
description: league.description,
ownerId: league.ownerId,
createdAt: league.createdAt,
maxDrivers: safeMaxDrivers,
usedDriverSlots,
maxTeams: undefined,
usedTeamSlots: undefined,
structureSummary,
scoringPatternSummary,
timingSummary,
scoring: scoringSummary,
};
});
this.viewModel = {
leagues: leagueItems,
totalCount: leagueItems.length,
};
return this.viewModel;
}
getViewModel(): AllLeaguesWithCapacityAndScoringViewModel {
if (!this.viewModel) {
throw new Error('Presenter has not been called yet');
}
return this.viewModel;
}
private deriveDropPolicySummary(config: {
championships: Array<{
dropScorePolicy: { strategy: string; count?: number; dropCount?: number };
}>;
}): string {
const championship = config.championships[0];
if (!championship) {
return 'All results count';
}
const policy = championship.dropScorePolicy;
if (!policy || policy.strategy === 'none') {
return 'All results count';
}
if (policy.strategy === 'bestNResults' && typeof policy.count === 'number') {
return `Best ${policy.count} results count`;
}
if (
policy.strategy === 'dropWorstN' &&
typeof policy.dropCount === 'number'
) {
return `Worst ${policy.dropCount} results are dropped`;
}
return 'Custom drop score rules';
}
}

View File

@@ -0,0 +1,58 @@
import type { League } from '@gridpilot/racing/domain/entities/League';
import type {
IAllLeaguesWithCapacityPresenter,
LeagueWithCapacityViewModel,
AllLeaguesWithCapacityViewModel,
} from '@gridpilot/racing/application/presenters/IAllLeaguesWithCapacityPresenter';
export class AllLeaguesWithCapacityPresenter implements IAllLeaguesWithCapacityPresenter {
private viewModel: AllLeaguesWithCapacityViewModel | null = null;
present(
leagues: League[],
memberCounts: Map<string, number>
): AllLeaguesWithCapacityViewModel {
const leagueItems: LeagueWithCapacityViewModel[] = leagues.map((league) => {
const usedSlots = memberCounts.get(league.id) ?? 0;
// Ensure we never expose an impossible state like 26/24:
// clamp maxDrivers to at least usedSlots at the application boundary.
const configuredMax = league.settings.maxDrivers ?? usedSlots;
const safeMaxDrivers = Math.max(configuredMax, usedSlots);
return {
id: league.id,
name: league.name,
description: league.description,
ownerId: league.ownerId,
settings: {
...league.settings,
maxDrivers: safeMaxDrivers,
},
createdAt: league.createdAt.toISOString(),
socialLinks: league.socialLinks
? {
discordUrl: league.socialLinks.discordUrl,
youtubeUrl: league.socialLinks.youtubeUrl,
websiteUrl: league.socialLinks.websiteUrl,
}
: undefined,
usedSlots,
};
});
this.viewModel = {
leagues: leagueItems,
totalCount: leagueItems.length,
};
return this.viewModel;
}
getViewModel(): AllLeaguesWithCapacityViewModel {
if (!this.viewModel) {
throw new Error('Presenter has not been called yet');
}
return this.viewModel;
}
}

View File

@@ -0,0 +1,38 @@
import type { Team } from '@gridpilot/racing/domain/entities/Team';
import type {
IAllTeamsPresenter,
TeamListItemViewModel,
AllTeamsViewModel,
} from '@gridpilot/racing/application/presenters/IAllTeamsPresenter';
export class AllTeamsPresenter implements IAllTeamsPresenter {
private viewModel: AllTeamsViewModel | null = null;
present(teams: Array<Team & { memberCount?: number }>): AllTeamsViewModel {
const teamItems: TeamListItemViewModel[] = teams.map((team) => ({
id: team.id,
name: team.name,
tag: team.tag,
description: team.description,
memberCount: team.memberCount ?? 0,
leagues: team.leagues,
specialization: team.specialization as 'endurance' | 'sprint' | 'mixed' | undefined,
region: team.region,
languages: team.languages,
}));
this.viewModel = {
teams: teamItems,
totalCount: teamItems.length,
};
return this.viewModel;
}
getViewModel(): AllTeamsViewModel {
if (!this.viewModel) {
throw new Error('Presenter has not been called yet');
}
return this.viewModel;
}
}

View File

@@ -0,0 +1,29 @@
import type {
IDriverRegistrationStatusPresenter,
DriverRegistrationStatusViewModel,
} from '@gridpilot/racing/application/presenters/IDriverRegistrationStatusPresenter';
export class DriverRegistrationStatusPresenter implements IDriverRegistrationStatusPresenter {
private viewModel: DriverRegistrationStatusViewModel | null = null;
present(
isRegistered: boolean,
raceId: string,
driverId: string
): DriverRegistrationStatusViewModel {
this.viewModel = {
isRegistered,
raceId,
driverId,
};
return this.viewModel;
}
getViewModel(): DriverRegistrationStatusViewModel {
if (!this.viewModel) {
throw new Error('Presenter has not been called yet');
}
return this.viewModel;
}
}

View File

@@ -0,0 +1,48 @@
import type { Team, TeamMembership } from '@gridpilot/racing/domain/entities/Team';
import type {
IDriverTeamPresenter,
DriverTeamViewModel,
} from '@gridpilot/racing/application/presenters/IDriverTeamPresenter';
export class DriverTeamPresenter implements IDriverTeamPresenter {
private viewModel: DriverTeamViewModel | null = null;
present(
team: Team,
membership: TeamMembership,
driverId: string
): DriverTeamViewModel {
const isOwner = team.ownerId === driverId;
const canManage = membership.role === 'owner' || membership.role === 'manager';
this.viewModel = {
team: {
id: team.id,
name: team.name,
tag: team.tag,
description: team.description,
ownerId: team.ownerId,
leagues: team.leagues,
specialization: team.specialization as 'endurance' | 'sprint' | 'mixed' | undefined,
region: team.region,
languages: team.languages,
},
membership: {
role: membership.role,
joinedAt: membership.joinedAt.toISOString(),
isActive: membership.isActive,
},
isOwner,
canManage,
};
return this.viewModel;
}
getViewModel(): DriverTeamViewModel {
if (!this.viewModel) {
throw new Error('Presenter has not been called yet');
}
return this.viewModel;
}
}

View File

@@ -0,0 +1,81 @@
import type { Driver } from '@gridpilot/racing/domain/entities/Driver';
import type { SkillLevel } from '@gridpilot/racing/domain/services/SkillLevelService';
import { SkillLevelService } from '@gridpilot/racing/domain/services/SkillLevelService';
import type {
IDriversLeaderboardPresenter,
DriverLeaderboardItemViewModel,
DriversLeaderboardViewModel,
} from '@gridpilot/racing/application/presenters/IDriversLeaderboardPresenter';
export class DriversLeaderboardPresenter implements IDriversLeaderboardPresenter {
private viewModel: DriversLeaderboardViewModel | null = null;
present(
drivers: Driver[],
rankings: Array<{ driverId: string; rating: number; overallRank: number }>,
stats: Record<string, { rating: number; wins: number; podiums: number; totalRaces: number; overallRank: number }>,
avatarUrls: Record<string, string>
): DriversLeaderboardViewModel {
const items: DriverLeaderboardItemViewModel[] = drivers.map((driver) => {
const driverStats = stats[driver.id];
const rating = driverStats?.rating ?? 0;
const wins = driverStats?.wins ?? 0;
const podiums = driverStats?.podiums ?? 0;
const totalRaces = driverStats?.totalRaces ?? 0;
let effectiveRank = Number.POSITIVE_INFINITY;
if (typeof driverStats?.overallRank === 'number' && driverStats.overallRank > 0) {
effectiveRank = driverStats.overallRank;
} else {
const indexInGlobal = rankings.findIndex((entry) => entry.driverId === driver.id);
if (indexInGlobal !== -1) {
effectiveRank = indexInGlobal + 1;
}
}
const skillLevel = SkillLevelService.getSkillLevel(rating);
const isActive = rankings.some((r) => r.driverId === driver.id);
return {
id: driver.id,
name: driver.name,
rating,
skillLevel,
nationality: driver.country,
racesCompleted: totalRaces,
wins,
podiums,
isActive,
rank: effectiveRank,
avatarUrl: avatarUrls[driver.id] ?? '',
};
});
items.sort((a, b) => {
const rankA = Number.isFinite(a.rank) && a.rank > 0 ? a.rank : Number.POSITIVE_INFINITY;
const rankB = Number.isFinite(b.rank) && b.rank > 0 ? b.rank : Number.POSITIVE_INFINITY;
if (rankA !== rankB) return rankA - rankB;
return b.rating - a.rating;
});
const totalRaces = items.reduce((sum, d) => sum + d.racesCompleted, 0);
const totalWins = items.reduce((sum, d) => sum + d.wins, 0);
const activeCount = items.filter((d) => d.isActive).length;
this.viewModel = {
drivers: items,
totalRaces,
totalWins,
activeCount,
};
return this.viewModel;
}
getViewModel(): DriversLeaderboardViewModel {
if (!this.viewModel) {
throw new Error('Presenter has not been called yet');
}
return this.viewModel;
}
}

View File

@@ -0,0 +1,14 @@
import type { IEntitySponsorshipPricingPresenter } from '@racing/application/presenters/IEntitySponsorshipPricingPresenter';
import type { GetEntitySponsorshipPricingResultDTO } from '@racing/application/use-cases/GetEntitySponsorshipPricingQuery';
export class EntitySponsorshipPricingPresenter implements IEntitySponsorshipPricingPresenter {
private data: GetEntitySponsorshipPricingResultDTO | null = null;
present(data: GetEntitySponsorshipPricingResultDTO | null): void {
this.data = data;
}
getData(): GetEntitySponsorshipPricingResultDTO | null {
return this.data;
}
}

View File

@@ -0,0 +1,78 @@
import type {
ILeagueDriverSeasonStatsPresenter,
LeagueDriverSeasonStatsItemViewModel,
LeagueDriverSeasonStatsViewModel,
} from '@gridpilot/racing/application/presenters/ILeagueDriverSeasonStatsPresenter';
export class LeagueDriverSeasonStatsPresenter implements ILeagueDriverSeasonStatsPresenter {
private viewModel: LeagueDriverSeasonStatsViewModel | null = null;
present(
leagueId: string,
standings: Array<{
driverId: string;
position: number;
points: number;
racesCompleted: number;
}>,
penalties: Map<string, { baseDelta: number; bonusDelta: number }>,
driverResults: Map<string, Array<{ position: number }>>,
driverRatings: Map<string, { rating: number | null; ratingChange: number | null }>
): LeagueDriverSeasonStatsViewModel {
const stats: LeagueDriverSeasonStatsItemViewModel[] = standings.map((standing) => {
const penalty = penalties.get(standing.driverId) ?? { baseDelta: 0, bonusDelta: 0 };
const totalPenaltyPoints = penalty.baseDelta;
const bonusPoints = penalty.bonusDelta;
const racesCompleted = standing.racesCompleted;
const pointsPerRace = racesCompleted > 0 ? standing.points / racesCompleted : 0;
const ratingInfo = driverRatings.get(standing.driverId) ?? { rating: null, ratingChange: null };
const results = driverResults.get(standing.driverId) ?? [];
let avgFinish: number | null = null;
if (results.length > 0) {
const totalPositions = results.reduce((sum, r) => sum + r.position, 0);
const avg = totalPositions / results.length;
avgFinish = Number.isFinite(avg) ? Number(avg.toFixed(2)) : null;
}
return {
leagueId,
driverId: standing.driverId,
position: standing.position,
driverName: '',
teamId: undefined,
teamName: undefined,
totalPoints: standing.points + totalPenaltyPoints + bonusPoints,
basePoints: standing.points,
penaltyPoints: Math.abs(totalPenaltyPoints),
bonusPoints,
pointsPerRace,
racesStarted: results.length,
racesFinished: results.length,
dnfs: 0,
noShows: 0,
avgFinish,
rating: ratingInfo.rating,
ratingChange: ratingInfo.ratingChange,
};
});
stats.sort((a, b) => a.position - b.position);
this.viewModel = {
leagueId,
stats,
};
return this.viewModel;
}
getViewModel(): LeagueDriverSeasonStatsViewModel {
if (!this.viewModel) {
throw new Error('Presenter has not been called yet');
}
return this.viewModel;
}
}

View File

@@ -0,0 +1,119 @@
import type { DropScorePolicy } from '@gridpilot/racing/domain/value-objects/DropScorePolicy';
import type {
ILeagueFullConfigPresenter,
LeagueFullConfigData,
LeagueConfigFormViewModel,
} from '@gridpilot/racing/application/presenters/ILeagueFullConfigPresenter';
export class LeagueFullConfigPresenter implements ILeagueFullConfigPresenter {
private viewModel: LeagueConfigFormViewModel | null = null;
present(data: LeagueFullConfigData): LeagueConfigFormViewModel {
const { league, activeSeason, scoringConfig, game } = data;
const patternId = scoringConfig?.scoringPresetId;
const primaryChampionship =
scoringConfig && scoringConfig.championships && scoringConfig.championships.length > 0
? scoringConfig.championships[0]
: undefined;
const dropPolicy = primaryChampionship?.dropScorePolicy ?? undefined;
const dropPolicyForm = this.mapDropPolicy(dropPolicy);
const defaultQualifyingMinutes = 30;
const defaultMainRaceMinutes = 40;
const mainRaceMinutes =
typeof league.settings.sessionDuration === 'number'
? league.settings.sessionDuration
: defaultMainRaceMinutes;
const qualifyingMinutes = defaultQualifyingMinutes;
const roundsPlanned = 8;
let sessionCount = 2;
if (
primaryChampionship &&
Array.isArray((primaryChampionship as any).sessionTypes) &&
(primaryChampionship as any).sessionTypes.length > 0
) {
sessionCount = (primaryChampionship as any).sessionTypes.length;
}
const practiceMinutes = 20;
const sprintRaceMinutes = patternId === 'sprint-main-driver' ? 20 : undefined;
this.viewModel = {
leagueId: league.id,
basics: {
name: league.name,
description: league.description,
visibility: 'public',
gameId: game?.id ?? 'iracing',
},
structure: {
mode: 'solo',
maxDrivers: league.settings.maxDrivers ?? 32,
maxTeams: undefined,
driversPerTeam: undefined,
multiClassEnabled: false,
},
championships: {
enableDriverChampionship: true,
enableTeamChampionship: false,
enableNationsChampionship: false,
enableTrophyChampionship: false,
},
scoring: {
patternId: patternId ?? undefined,
customScoringEnabled: !patternId,
},
dropPolicy: dropPolicyForm,
timings: {
practiceMinutes,
qualifyingMinutes,
sprintRaceMinutes,
mainRaceMinutes,
sessionCount,
roundsPlanned,
},
stewarding: {
decisionMode: 'admin_only',
requireDefense: true,
defenseTimeLimit: 48,
voteTimeLimit: 72,
protestDeadlineHours: 72,
stewardingClosesHours: 168,
notifyAccusedOnProtest: true,
notifyOnVoteRequired: true,
},
};
return this.viewModel;
}
getViewModel(): LeagueConfigFormViewModel {
if (!this.viewModel) {
throw new Error('Presenter has not been called yet');
}
return this.viewModel;
}
private mapDropPolicy(policy: DropScorePolicy | undefined): { strategy: string; n?: number } {
if (!policy || policy.strategy === 'none') {
return { strategy: 'none' };
}
if (policy.strategy === 'bestNResults') {
const n = typeof policy.count === 'number' ? policy.count : undefined;
return n !== undefined ? { strategy: 'bestNResults', n } : { strategy: 'none' };
}
if (policy.strategy === 'dropWorstN') {
const n = typeof policy.dropCount === 'number' ? policy.dropCount : undefined;
return n !== undefined ? { strategy: 'dropWorstN', n } : { strategy: 'none' };
}
return { strategy: 'none' };
}
}

View File

@@ -0,0 +1,14 @@
import type { ILeagueSchedulePreviewPresenter } from '@racing/application/presenters/ILeagueSchedulePreviewPresenter';
import type { LeagueSchedulePreviewDTO } from '@racing/application/dto/LeagueScheduleDTO';
export class LeagueSchedulePreviewPresenter implements ILeagueSchedulePreviewPresenter {
private data: LeagueSchedulePreviewDTO | null = null;
present(data: LeagueSchedulePreviewDTO): void {
this.data = data;
}
getData(): LeagueSchedulePreviewDTO | null {
return this.data;
}
}

View File

@@ -0,0 +1,149 @@
import type { ChampionshipConfig } from '@gridpilot/racing/domain/value-objects/ChampionshipConfig';
import type { BonusRule } from '@gridpilot/racing/domain/value-objects/BonusRule';
import type {
ILeagueScoringConfigPresenter,
LeagueScoringConfigData,
LeagueScoringConfigViewModel,
LeagueScoringChampionshipViewModel,
} from '@gridpilot/racing/application/presenters/ILeagueScoringConfigPresenter';
export class LeagueScoringConfigPresenter implements ILeagueScoringConfigPresenter {
private viewModel: LeagueScoringConfigViewModel | null = null;
present(data: LeagueScoringConfigData): LeagueScoringConfigViewModel {
const championships: LeagueScoringChampionshipViewModel[] =
data.championships.map((champ) => this.mapChampionship(champ));
const dropPolicySummary =
data.preset?.dropPolicySummary ??
this.deriveDropPolicyDescriptionFromChampionships(data.championships);
this.viewModel = {
leagueId: data.leagueId,
seasonId: data.seasonId,
gameId: data.gameId,
gameName: data.gameName,
scoringPresetId: data.scoringPresetId,
scoringPresetName: data.preset?.name,
dropPolicySummary,
championships,
};
return this.viewModel;
}
getViewModel(): LeagueScoringConfigViewModel {
if (!this.viewModel) {
throw new Error('Presenter has not been called yet');
}
return this.viewModel;
}
private mapChampionship(championship: ChampionshipConfig): LeagueScoringChampionshipViewModel {
const sessionTypes = championship.sessionTypes.map((s) => s.toString());
const pointsPreview = this.buildPointsPreview(championship.pointsTableBySessionType);
const bonusSummary = this.buildBonusSummary(
championship.bonusRulesBySessionType ?? {},
);
const dropPolicyDescription = this.deriveDropPolicyDescription(
championship.dropScorePolicy,
);
return {
id: championship.id,
name: championship.name,
type: championship.type,
sessionTypes,
pointsPreview,
bonusSummary,
dropPolicyDescription,
};
}
private buildPointsPreview(
tables: Record<string, any>,
): Array<{ sessionType: string; position: number; points: number }> {
const preview: Array<{
sessionType: string;
position: number;
points: number;
}> = [];
const maxPositions = 10;
for (const [sessionType, table] of Object.entries(tables)) {
for (let pos = 1; pos <= maxPositions; pos++) {
const points = table.getPointsForPosition(pos);
if (points && points !== 0) {
preview.push({
sessionType,
position: pos,
points,
});
}
}
}
return preview;
}
private buildBonusSummary(
bonusRulesBySessionType: Record<string, BonusRule[]>,
): string[] {
const summaries: string[] = [];
for (const [sessionType, rules] of Object.entries(bonusRulesBySessionType)) {
for (const rule of rules) {
if (rule.type === 'fastestLap') {
const base = `Fastest lap in ${sessionType}`;
if (rule.requiresFinishInTopN) {
summaries.push(
`${base} +${rule.points} points if finishing P${rule.requiresFinishInTopN} or better`,
);
} else {
summaries.push(`${base} +${rule.points} points`);
}
} else {
summaries.push(
`${rule.type} bonus in ${sessionType} worth ${rule.points} points`,
);
}
}
}
return summaries;
}
private deriveDropPolicyDescriptionFromChampionships(
championships: ChampionshipConfig[],
): string {
const first = championships[0];
if (!first) {
return 'All results count';
}
return this.deriveDropPolicyDescription(first.dropScorePolicy);
}
private deriveDropPolicyDescription(policy: {
strategy: string;
count?: number;
dropCount?: number;
}): string {
if (!policy || policy.strategy === 'none') {
return 'All results count';
}
if (policy.strategy === 'bestNResults' && typeof policy.count === 'number') {
return `Best ${policy.count} results count towards the championship`;
}
if (
policy.strategy === 'dropWorstN' &&
typeof policy.dropCount === 'number'
) {
return `Worst ${policy.dropCount} results are dropped from the championship total`;
}
return 'Custom drop score rules apply';
}
}

View File

@@ -0,0 +1,25 @@
import type { LeagueScoringPresetDTO } from '@gridpilot/racing/application/ports/LeagueScoringPresetProvider';
import type {
ILeagueScoringPresetsPresenter,
LeagueScoringPresetsViewModel,
} from '@gridpilot/racing/application/presenters/ILeagueScoringPresetsPresenter';
export class LeagueScoringPresetsPresenter implements ILeagueScoringPresetsPresenter {
private viewModel: LeagueScoringPresetsViewModel | null = null;
present(presets: LeagueScoringPresetDTO[]): LeagueScoringPresetsViewModel {
this.viewModel = {
presets,
totalCount: presets.length,
};
return this.viewModel;
}
getViewModel(): LeagueScoringPresetsViewModel {
if (!this.viewModel) {
throw new Error('Presenter has not been called yet');
}
return this.viewModel;
}
}

View File

@@ -0,0 +1,38 @@
import type { Standing } from '@gridpilot/racing/domain/entities/Standing';
import type {
ILeagueStandingsPresenter,
StandingItemViewModel,
LeagueStandingsViewModel,
} from '@gridpilot/racing/application/presenters/ILeagueStandingsPresenter';
export class LeagueStandingsPresenter implements ILeagueStandingsPresenter {
private viewModel: LeagueStandingsViewModel | null = null;
present(standings: Standing[]): LeagueStandingsViewModel {
const standingItems: StandingItemViewModel[] = standings.map((standing) => ({
id: standing.id,
leagueId: standing.leagueId,
seasonId: standing.seasonId,
driverId: standing.driverId,
position: standing.position,
points: standing.points,
wins: standing.wins,
podiums: standing.podiums,
racesCompleted: standing.racesCompleted,
}));
this.viewModel = {
leagueId: standings[0]?.leagueId ?? '',
standings: standingItems,
};
return this.viewModel;
}
getViewModel(): LeagueStandingsViewModel {
if (!this.viewModel) {
throw new Error('Presenter has not been called yet');
}
return this.viewModel;
}
}

View File

@@ -0,0 +1,42 @@
import type {
ILeagueStatsPresenter,
LeagueStatsViewModel,
} from '@gridpilot/racing/application/presenters/ILeagueStatsPresenter';
export class LeagueStatsPresenter implements ILeagueStatsPresenter {
private viewModel: LeagueStatsViewModel | null = null;
present(
leagueId: string,
totalRaces: number,
completedRaces: number,
scheduledRaces: number,
sofValues: number[]
): LeagueStatsViewModel {
const averageSOF = sofValues.length > 0
? Math.round(sofValues.reduce((a, b) => a + b, 0) / sofValues.length)
: null;
const highestSOF = sofValues.length > 0 ? Math.max(...sofValues) : null;
const lowestSOF = sofValues.length > 0 ? Math.min(...sofValues) : null;
this.viewModel = {
leagueId,
totalRaces,
completedRaces,
scheduledRaces,
averageSOF,
highestSOF,
lowestSOF,
};
return this.viewModel;
}
getViewModel(): LeagueStatsViewModel {
if (!this.viewModel) {
throw new Error('Presenter has not been called yet');
}
return this.viewModel;
}
}

View File

@@ -0,0 +1,14 @@
import type { IPendingSponsorshipRequestsPresenter } from '@racing/application/presenters/IPendingSponsorshipRequestsPresenter';
import type { GetPendingSponsorshipRequestsResultDTO } from '@racing/application/use-cases/GetPendingSponsorshipRequestsQuery';
export class PendingSponsorshipRequestsPresenter implements IPendingSponsorshipRequestsPresenter {
private data: GetPendingSponsorshipRequestsResultDTO | null = null;
present(data: GetPendingSponsorshipRequestsResultDTO): void {
this.data = data;
}
getData(): GetPendingSponsorshipRequestsResultDTO | null {
return this.data;
}
}

View File

@@ -0,0 +1,60 @@
import type {
IRacePenaltiesPresenter,
RacePenaltyViewModel,
RacePenaltiesViewModel,
} from '@gridpilot/racing/application/presenters/IRacePenaltiesPresenter';
import type { PenaltyType, PenaltyStatus } from '@gridpilot/racing/domain/entities/Penalty';
export class RacePenaltiesPresenter implements IRacePenaltiesPresenter {
private viewModel: RacePenaltiesViewModel | null = null;
present(
penalties: Array<{
id: string;
raceId: string;
driverId: string;
type: PenaltyType;
value?: number;
reason: string;
protestId?: string;
issuedBy: string;
status: PenaltyStatus;
issuedAt: Date;
appliedAt?: Date;
notes?: string;
getDescription(): string;
}>,
driverMap: Map<string, string>
): RacePenaltiesViewModel {
const penaltyViewModels: RacePenaltyViewModel[] = penalties.map(penalty => ({
id: penalty.id,
raceId: penalty.raceId,
driverId: penalty.driverId,
driverName: driverMap.get(penalty.driverId) || 'Unknown',
type: penalty.type,
value: penalty.value,
reason: penalty.reason,
protestId: penalty.protestId,
issuedBy: penalty.issuedBy,
issuedByName: driverMap.get(penalty.issuedBy) || 'Unknown',
status: penalty.status,
description: penalty.getDescription(),
issuedAt: penalty.issuedAt.toISOString(),
appliedAt: penalty.appliedAt?.toISOString(),
notes: penalty.notes,
}));
this.viewModel = {
penalties: penaltyViewModels,
};
return this.viewModel;
}
getViewModel(): RacePenaltiesViewModel {
if (!this.viewModel) {
throw new Error('Presenter has not been called yet');
}
return this.viewModel;
}
}

View File

@@ -0,0 +1,59 @@
import type {
IRaceProtestsPresenter,
RaceProtestViewModel,
RaceProtestsViewModel,
} from '@gridpilot/racing/application/presenters/IRaceProtestsPresenter';
import type { ProtestStatus, ProtestIncident } from '@gridpilot/racing/domain/entities/Protest';
export class RaceProtestsPresenter implements IRaceProtestsPresenter {
private viewModel: RaceProtestsViewModel | null = null;
present(
protests: Array<{
id: string;
raceId: string;
protestingDriverId: string;
accusedDriverId: string;
incident: ProtestIncident;
comment?: string;
proofVideoUrl?: string;
status: ProtestStatus;
reviewedBy?: string;
decisionNotes?: string;
filedAt: Date;
reviewedAt?: Date;
}>,
driverMap: Map<string, string>
): RaceProtestsViewModel {
const protestViewModels: RaceProtestViewModel[] = protests.map(protest => ({
id: protest.id,
raceId: protest.raceId,
protestingDriverId: protest.protestingDriverId,
protestingDriverName: driverMap.get(protest.protestingDriverId) || 'Unknown',
accusedDriverId: protest.accusedDriverId,
accusedDriverName: driverMap.get(protest.accusedDriverId) || 'Unknown',
incident: protest.incident,
comment: protest.comment,
proofVideoUrl: protest.proofVideoUrl,
status: protest.status,
reviewedBy: protest.reviewedBy,
reviewedByName: protest.reviewedBy ? driverMap.get(protest.reviewedBy) : undefined,
decisionNotes: protest.decisionNotes,
filedAt: protest.filedAt.toISOString(),
reviewedAt: protest.reviewedAt?.toISOString(),
}));
this.viewModel = {
protests: protestViewModels,
};
return this.viewModel;
}
getViewModel(): RaceProtestsViewModel {
if (!this.viewModel) {
throw new Error('Presenter has not been called yet');
}
return this.viewModel;
}
}

View File

@@ -0,0 +1,24 @@
import type {
IRaceRegistrationsPresenter,
RaceRegistrationsViewModel,
} from '@gridpilot/racing/application/presenters/IRaceRegistrationsPresenter';
export class RaceRegistrationsPresenter implements IRaceRegistrationsPresenter {
private viewModel: RaceRegistrationsViewModel | null = null;
present(registeredDriverIds: string[]): RaceRegistrationsViewModel {
this.viewModel = {
registeredDriverIds,
count: registeredDriverIds.length,
};
return this.viewModel;
}
getViewModel(): RaceRegistrationsViewModel {
if (!this.viewModel) {
throw new Error('Presenter has not been called yet');
}
return this.viewModel;
}
}

View File

@@ -0,0 +1,49 @@
import type {
IRaceWithSOFPresenter,
RaceWithSOFViewModel,
} from '@gridpilot/racing/application/presenters/IRaceWithSOFPresenter';
export class RaceWithSOFPresenter implements IRaceWithSOFPresenter {
private viewModel: RaceWithSOFViewModel | null = null;
present(
raceId: string,
leagueId: string,
scheduledAt: Date,
track: string,
trackId: string,
car: string,
carId: string,
sessionType: string,
status: string,
strengthOfField: number | null,
registeredCount: number,
maxParticipants: number,
participantCount: number
): RaceWithSOFViewModel {
this.viewModel = {
id: raceId,
leagueId,
scheduledAt: scheduledAt.toISOString(),
track,
trackId,
car,
carId,
sessionType,
status,
strengthOfField,
registeredCount,
maxParticipants,
participantCount,
};
return this.viewModel;
}
getViewModel(): RaceWithSOFViewModel {
if (!this.viewModel) {
throw new Error('Presenter has not been called yet');
}
return this.viewModel;
}
}

View File

@@ -0,0 +1,64 @@
import type {
IRacesPagePresenter,
RacesPageViewModel,
RaceListItemViewModel,
} from '@gridpilot/racing/application/presenters/IRacesPagePresenter';
export class RacesPagePresenter implements IRacesPagePresenter {
private viewModel: RacesPageViewModel | null = null;
present(races: any[]): void {
const now = new Date();
const nextWeek = new Date(now.getTime() + 7 * 24 * 60 * 60 * 1000);
const raceViewModels: RaceListItemViewModel[] = races.map(race => ({
id: race.id,
track: race.track,
car: race.car,
scheduledAt: race.scheduledAt,
status: race.status,
leagueId: race.leagueId,
leagueName: race.leagueName,
strengthOfField: race.strengthOfField,
isUpcoming: race.isUpcoming,
isLive: race.isLive,
isPast: race.isPast,
}));
const stats = {
total: raceViewModels.length,
scheduled: raceViewModels.filter(r => r.status === 'scheduled').length,
running: raceViewModels.filter(r => r.status === 'running').length,
completed: raceViewModels.filter(r => r.status === 'completed').length,
};
const liveRaces = raceViewModels.filter(r => r.isLive);
const upcomingThisWeek = raceViewModels
.filter(r => {
const scheduledDate = new Date(r.scheduledAt);
return r.isUpcoming && scheduledDate >= now && scheduledDate <= nextWeek;
})
.slice(0, 5);
const recentResults = raceViewModels
.filter(r => r.status === 'completed')
.sort((a, b) => new Date(b.scheduledAt).getTime() - new Date(a.scheduledAt).getTime())
.slice(0, 3);
this.viewModel = {
races: raceViewModels,
stats,
liveRaces,
upcomingThisWeek,
recentResults,
};
}
getViewModel(): RacesPageViewModel {
if (!this.viewModel) {
throw new Error('ViewModel not yet generated. Call present() first.');
}
return this.viewModel;
}
}

View File

@@ -0,0 +1,14 @@
import type { ISponsorDashboardPresenter } from '@racing/application/presenters/ISponsorDashboardPresenter';
import type { SponsorDashboardDTO } from '@racing/application/use-cases/GetSponsorDashboardQuery';
export class SponsorDashboardPresenter implements ISponsorDashboardPresenter {
private data: SponsorDashboardDTO | null = null;
present(data: SponsorDashboardDTO | null): void {
this.data = data;
}
getData(): SponsorDashboardDTO | null {
return this.data;
}
}

View File

@@ -0,0 +1,14 @@
import type { ISponsorSponsorshipsPresenter } from '@racing/application/presenters/ISponsorSponsorshipsPresenter';
import type { SponsorSponsorshipsDTO } from '@racing/application/use-cases/GetSponsorSponsorshipsQuery';
export class SponsorSponsorshipsPresenter implements ISponsorSponsorshipsPresenter {
private data: SponsorSponsorshipsDTO | null = null;
present(data: SponsorSponsorshipsDTO | null): void {
this.data = data;
}
getData(): SponsorSponsorshipsDTO | null {
return this.data;
}
}

View File

@@ -0,0 +1,48 @@
import type { Team, TeamMembership } from '@gridpilot/racing/domain/entities/Team';
import type {
ITeamDetailsPresenter,
TeamDetailsViewModel,
} from '@gridpilot/racing/application/presenters/ITeamDetailsPresenter';
export class TeamDetailsPresenter implements ITeamDetailsPresenter {
private viewModel: TeamDetailsViewModel | null = null;
present(
team: Team,
membership: TeamMembership | null,
driverId: string
): TeamDetailsViewModel {
const canManage = membership?.role === 'owner' || membership?.role === 'manager';
this.viewModel = {
team: {
id: team.id,
name: team.name,
tag: team.tag,
description: team.description,
ownerId: team.ownerId,
leagues: team.leagues,
specialization: team.specialization as 'endurance' | 'sprint' | 'mixed' | undefined,
region: team.region,
languages: team.languages,
},
membership: membership
? {
role: membership.role,
joinedAt: membership.joinedAt.toISOString(),
isActive: membership.isActive,
}
: null,
canManage,
};
return this.viewModel;
}
getViewModel(): TeamDetailsViewModel {
if (!this.viewModel) {
throw new Error('Presenter has not been called yet');
}
return this.viewModel;
}
}

View File

@@ -0,0 +1,43 @@
import type { TeamJoinRequest } from '@gridpilot/racing/domain/entities/Team';
import type {
ITeamJoinRequestsPresenter,
TeamJoinRequestViewModel,
TeamJoinRequestsViewModel,
} from '@gridpilot/racing/application/presenters/ITeamJoinRequestsPresenter';
export class TeamJoinRequestsPresenter implements ITeamJoinRequestsPresenter {
private viewModel: TeamJoinRequestsViewModel | null = null;
present(
requests: TeamJoinRequest[],
driverNames: Record<string, string>,
avatarUrls: Record<string, string>
): TeamJoinRequestsViewModel {
const requestItems: TeamJoinRequestViewModel[] = requests.map((request) => ({
requestId: request.id,
driverId: request.driverId,
driverName: driverNames[request.driverId] ?? 'Unknown Driver',
teamId: request.teamId,
status: request.status,
requestedAt: request.requestedAt.toISOString(),
avatarUrl: avatarUrls[request.driverId] ?? '',
}));
const pendingCount = requestItems.filter((r) => r.status === 'pending').length;
this.viewModel = {
requests: requestItems,
pendingCount,
totalCount: requestItems.length,
};
return this.viewModel;
}
getViewModel(): TeamJoinRequestsViewModel {
if (!this.viewModel) {
throw new Error('Presenter has not been called yet');
}
return this.viewModel;
}
}

View File

@@ -0,0 +1,46 @@
import type { TeamMembership } from '@gridpilot/racing/domain/entities/Team';
import type {
ITeamMembersPresenter,
TeamMemberViewModel,
TeamMembersViewModel,
} from '@gridpilot/racing/application/presenters/ITeamMembersPresenter';
export class TeamMembersPresenter implements ITeamMembersPresenter {
private viewModel: TeamMembersViewModel | null = null;
present(
memberships: TeamMembership[],
driverNames: Record<string, string>,
avatarUrls: Record<string, string>
): TeamMembersViewModel {
const members: TeamMemberViewModel[] = memberships.map((membership) => ({
driverId: membership.driverId,
driverName: driverNames[membership.driverId] ?? 'Unknown Driver',
role: membership.role,
joinedAt: membership.joinedAt.toISOString(),
isActive: membership.isActive,
avatarUrl: avatarUrls[membership.driverId] ?? '',
}));
const ownerCount = members.filter((m) => m.role === 'owner').length;
const managerCount = members.filter((m) => m.role === 'manager').length;
const memberCount = members.filter((m) => m.role === 'member').length;
this.viewModel = {
members,
totalCount: members.length,
ownerCount,
managerCount,
memberCount,
};
return this.viewModel;
}
getViewModel(): TeamMembersViewModel {
if (!this.viewModel) {
throw new Error('Presenter has not been called yet');
}
return this.viewModel;
}
}

View File

@@ -0,0 +1,42 @@
import type {
ITeamsLeaderboardPresenter,
TeamsLeaderboardViewModel,
TeamLeaderboardItemViewModel,
SkillLevel,
} from '@gridpilot/racing/application/presenters/ITeamsLeaderboardPresenter';
export class TeamsLeaderboardPresenter implements ITeamsLeaderboardPresenter {
private viewModel: TeamsLeaderboardViewModel | null = null;
present(teams: any[], recruitingCount: number): void {
this.viewModel = {
teams: teams.map((team) => this.transformTeam(team)),
recruitingCount,
};
}
getViewModel(): TeamsLeaderboardViewModel {
if (!this.viewModel) {
throw new Error('ViewModel not yet generated. Call present() first.');
}
return this.viewModel;
}
private transformTeam(team: any): TeamLeaderboardItemViewModel {
return {
id: team.id,
name: team.name,
memberCount: team.memberCount,
rating: team.rating,
totalWins: team.totalWins,
totalRaces: team.totalRaces,
performanceLevel: team.performanceLevel as SkillLevel,
isRecruiting: team.isRecruiting,
createdAt: team.createdAt,
description: team.description,
specialization: team.specialization,
region: team.region,
languages: team.languages,
};
}
}

View File

@@ -0,0 +1,47 @@
import type { League } from '../../domain/entities/League';
import type { Season } from '../../domain/entities/Season';
import type { LeagueScoringConfig } from '../../domain/entities/LeagueScoringConfig';
import type { Game } from '../../domain/entities/Game';
import type { LeagueScoringPresetDTO } from '../ports/LeagueScoringPresetProvider';
export interface LeagueSummaryViewModel {
id: string;
name: string;
description: string;
ownerId: string;
createdAt: Date;
maxDrivers: number;
usedDriverSlots: number;
maxTeams?: number;
usedTeamSlots?: number;
structureSummary: string;
scoringPatternSummary?: string;
timingSummary: string;
scoring?: {
gameId: string;
gameName: string;
primaryChampionshipType: string;
scoringPresetId: string;
scoringPresetName: string;
dropPolicySummary: string;
scoringPatternSummary: string;
};
}
export interface AllLeaguesWithCapacityAndScoringViewModel {
leagues: LeagueSummaryViewModel[];
totalCount: number;
}
export interface LeagueEnrichedData {
league: League;
usedDriverSlots: number;
season?: Season;
scoringConfig?: LeagueScoringConfig;
game?: Game;
preset?: LeagueScoringPresetDTO;
}
export interface IAllLeaguesWithCapacityAndScoringPresenter {
present(enrichedLeagues: LeagueEnrichedData[]): AllLeaguesWithCapacityAndScoringViewModel;
}

View File

@@ -0,0 +1,32 @@
import type { League } from '../../domain/entities/League';
export interface LeagueWithCapacityViewModel {
id: string;
name: string;
description: string;
ownerId: string;
settings: {
maxDrivers: number;
sessionDuration?: number;
visibility?: string;
};
createdAt: string;
socialLinks?: {
discordUrl?: string;
youtubeUrl?: string;
websiteUrl?: string;
};
usedSlots: number;
}
export interface AllLeaguesWithCapacityViewModel {
leagues: LeagueWithCapacityViewModel[];
totalCount: number;
}
export interface IAllLeaguesWithCapacityPresenter {
present(
leagues: League[],
memberCounts: Map<string, number>
): AllLeaguesWithCapacityViewModel;
}

View File

@@ -0,0 +1,22 @@
import type { Team } from '../../domain/entities/Team';
export interface TeamListItemViewModel {
id: string;
name: string;
tag: string;
description: string;
memberCount: number;
leagues: string[];
specialization?: 'endurance' | 'sprint' | 'mixed';
region?: string;
languages?: string[];
}
export interface AllTeamsViewModel {
teams: TeamListItemViewModel[];
totalCount: number;
}
export interface IAllTeamsPresenter {
present(teams: Team[]): AllTeamsViewModel;
}

View File

@@ -0,0 +1,14 @@
export interface DriverRegistrationStatusViewModel {
isRegistered: boolean;
raceId: string;
driverId: string;
}
export interface IDriverRegistrationStatusPresenter {
present(
isRegistered: boolean,
raceId: string,
driverId: string
): DriverRegistrationStatusViewModel;
getViewModel(): DriverRegistrationStatusViewModel;
}

View File

@@ -0,0 +1,30 @@
import type { Team, TeamMembership } from '../../domain/entities/Team';
export interface DriverTeamViewModel {
team: {
id: string;
name: string;
tag: string;
description: string;
ownerId: string;
leagues: string[];
specialization?: 'endurance' | 'sprint' | 'mixed';
region?: string;
languages?: string[];
};
membership: {
role: 'owner' | 'manager' | 'member';
joinedAt: string;
isActive: boolean;
};
isOwner: boolean;
canManage: boolean;
}
export interface IDriverTeamPresenter {
present(
team: Team,
membership: TeamMembership,
driverId: string
): DriverTeamViewModel;
}

View File

@@ -0,0 +1,34 @@
import type { Driver } from '../../domain/entities/Driver';
import type { SkillLevel } from '../../domain/services/SkillLevelService';
export type { SkillLevel };
export interface DriverLeaderboardItemViewModel {
id: string;
name: string;
rating: number;
skillLevel: SkillLevel;
nationality: string;
racesCompleted: number;
wins: number;
podiums: number;
isActive: boolean;
rank: number;
avatarUrl: string;
}
export interface DriversLeaderboardViewModel {
drivers: DriverLeaderboardItemViewModel[];
totalRaces: number;
totalWins: number;
activeCount: number;
}
export interface IDriversLeaderboardPresenter {
present(
drivers: Driver[],
rankings: Array<{ driverId: string; rating: number; overallRank: number }>,
stats: Record<string, { rating: number; wins: number; podiums: number; totalRaces: number; overallRank: number }>,
avatarUrls: Record<string, string>
): DriversLeaderboardViewModel;
}

View File

@@ -0,0 +1,5 @@
import type { GetEntitySponsorshipPricingResultDTO } from '../use-cases/GetEntitySponsorshipPricingQuery';
export interface IEntitySponsorshipPricingPresenter {
present(data: GetEntitySponsorshipPricingResultDTO | null): void;
}

View File

@@ -0,0 +1,40 @@
export interface LeagueDriverSeasonStatsItemViewModel {
leagueId: string;
driverId: string;
position: number;
driverName: string;
teamId?: string;
teamName?: string;
totalPoints: number;
basePoints: number;
penaltyPoints: number;
bonusPoints: number;
pointsPerRace: number;
racesStarted: number;
racesFinished: number;
dnfs: number;
noShows: number;
avgFinish: number | null;
rating: number | null;
ratingChange: number | null;
}
export interface LeagueDriverSeasonStatsViewModel {
leagueId: string;
stats: LeagueDriverSeasonStatsItemViewModel[];
}
export interface ILeagueDriverSeasonStatsPresenter {
present(
leagueId: string,
standings: Array<{
driverId: string;
position: number;
points: number;
racesCompleted: number;
}>,
penalties: Map<string, { baseDelta: number; bonusDelta: number }>,
driverResults: Map<string, Array<{ position: number }>>,
driverRatings: Map<string, { rating: number | null; ratingChange: number | null }>
): LeagueDriverSeasonStatsViewModel;
}

View File

@@ -0,0 +1,64 @@
import type { League } from '../../domain/entities/League';
import type { Season } from '../../domain/entities/Season';
import type { LeagueScoringConfig } from '../../domain/entities/LeagueScoringConfig';
import type { Game } from '../../domain/entities/Game';
export interface LeagueConfigFormViewModel {
leagueId: string;
basics: {
name: string;
description: string;
visibility: string;
gameId: string;
};
structure: {
mode: string;
maxDrivers: number;
maxTeams?: number;
driversPerTeam?: number;
multiClassEnabled: boolean;
};
championships: {
enableDriverChampionship: boolean;
enableTeamChampionship: boolean;
enableNationsChampionship: boolean;
enableTrophyChampionship: boolean;
};
scoring: {
patternId?: string;
customScoringEnabled: boolean;
};
dropPolicy: {
strategy: string;
n?: number;
};
timings: {
practiceMinutes: number;
qualifyingMinutes: number;
sprintRaceMinutes?: number;
mainRaceMinutes: number;
sessionCount: number;
roundsPlanned: number;
};
stewarding: {
decisionMode: string;
requireDefense: boolean;
defenseTimeLimit: number;
voteTimeLimit: number;
protestDeadlineHours: number;
stewardingClosesHours: number;
notifyAccusedOnProtest: boolean;
notifyOnVoteRequired: boolean;
};
}
export interface LeagueFullConfigData {
league: League;
activeSeason?: Season;
scoringConfig?: LeagueScoringConfig;
game?: Game;
}
export interface ILeagueFullConfigPresenter {
present(data: LeagueFullConfigData): LeagueConfigFormViewModel;
}

View File

@@ -0,0 +1,5 @@
import type { LeagueSchedulePreviewDTO } from '../dto/LeagueScheduleDTO';
export interface ILeagueSchedulePreviewPresenter {
present(data: LeagueSchedulePreviewDTO): void;
}

View File

@@ -0,0 +1,38 @@
import type { ChampionshipConfig } from '../../domain/value-objects/ChampionshipConfig';
import type { LeagueScoringPresetDTO } from '../ports/LeagueScoringPresetProvider';
export interface LeagueScoringChampionshipViewModel {
id: string;
name: string;
type: string;
sessionTypes: string[];
pointsPreview: Array<{ sessionType: string; position: number; points: number }>;
bonusSummary: string[];
dropPolicyDescription: string;
}
export interface LeagueScoringConfigViewModel {
leagueId: string;
seasonId: string;
gameId: string;
gameName: string;
scoringPresetId?: string;
scoringPresetName?: string;
dropPolicySummary: string;
championships: LeagueScoringChampionshipViewModel[];
}
export interface LeagueScoringConfigData {
leagueId: string;
seasonId: string;
gameId: string;
gameName: string;
scoringPresetId?: string;
preset?: LeagueScoringPresetDTO;
championships: ChampionshipConfig[];
}
export interface ILeagueScoringConfigPresenter {
present(data: LeagueScoringConfigData): LeagueScoringConfigViewModel;
getViewModel(): LeagueScoringConfigViewModel;
}

View File

@@ -0,0 +1,10 @@
import type { LeagueScoringPresetDTO } from '../ports/LeagueScoringPresetProvider';
export interface LeagueScoringPresetsViewModel {
presets: LeagueScoringPresetDTO[];
totalCount: number;
}
export interface ILeagueScoringPresetsPresenter {
present(presets: LeagueScoringPresetDTO[]): LeagueScoringPresetsViewModel;
}

View File

@@ -0,0 +1,22 @@
import type { Standing } from '../../domain/entities/Standing';
export interface StandingItemViewModel {
id: string;
leagueId: string;
seasonId: string;
driverId: string;
position: number;
points: number;
wins: number;
podiums: number;
racesCompleted: number;
}
export interface LeagueStandingsViewModel {
leagueId: string;
standings: StandingItemViewModel[];
}
export interface ILeagueStandingsPresenter {
present(standings: Standing[]): LeagueStandingsViewModel;
}

View File

@@ -0,0 +1,20 @@
export interface LeagueStatsViewModel {
leagueId: string;
totalRaces: number;
completedRaces: number;
scheduledRaces: number;
averageSOF: number | null;
highestSOF: number | null;
lowestSOF: number | null;
}
export interface ILeagueStatsPresenter {
present(
leagueId: string,
totalRaces: number,
completedRaces: number,
scheduledRaces: number,
sofValues: number[]
): LeagueStatsViewModel;
getViewModel(): LeagueStatsViewModel;
}

View File

@@ -0,0 +1,5 @@
import type { GetPendingSponsorshipRequestsResultDTO } from '../use-cases/GetPendingSponsorshipRequestsQuery';
export interface IPendingSponsorshipRequestsPresenter {
present(data: GetPendingSponsorshipRequestsResultDTO): void;
}

View File

@@ -0,0 +1,44 @@
import type { PenaltyType, PenaltyStatus } from '../../domain/entities/Penalty';
export interface RacePenaltyViewModel {
id: string;
raceId: string;
driverId: string;
driverName: string;
type: PenaltyType;
value?: number;
reason: string;
protestId?: string;
issuedBy: string;
issuedByName: string;
status: PenaltyStatus;
description: string;
issuedAt: string;
appliedAt?: string;
notes?: string;
}
export interface RacePenaltiesViewModel {
penalties: RacePenaltyViewModel[];
}
export interface IRacePenaltiesPresenter {
present(
penalties: Array<{
id: string;
raceId: string;
driverId: string;
type: PenaltyType;
value?: number;
reason: string;
protestId?: string;
issuedBy: string;
status: PenaltyStatus;
issuedAt: Date;
appliedAt?: Date;
notes?: string;
getDescription(): string;
}>,
driverMap: Map<string, string>
): RacePenaltiesViewModel;
}

View File

@@ -0,0 +1,43 @@
import type { ProtestStatus, ProtestIncident } from '../../domain/entities/Protest';
export interface RaceProtestViewModel {
id: string;
raceId: string;
protestingDriverId: string;
protestingDriverName: string;
accusedDriverId: string;
accusedDriverName: string;
incident: ProtestIncident;
comment?: string;
proofVideoUrl?: string;
status: ProtestStatus;
reviewedBy?: string;
reviewedByName?: string;
decisionNotes?: string;
filedAt: string;
reviewedAt?: string;
}
export interface RaceProtestsViewModel {
protests: RaceProtestViewModel[];
}
export interface IRaceProtestsPresenter {
present(
protests: Array<{
id: string;
raceId: string;
protestingDriverId: string;
accusedDriverId: string;
incident: ProtestIncident;
comment?: string;
proofVideoUrl?: string;
status: ProtestStatus;
reviewedBy?: string;
decisionNotes?: string;
filedAt: Date;
reviewedAt?: Date;
}>,
driverMap: Map<string, string>
): RaceProtestsViewModel;
}

View File

@@ -0,0 +1,9 @@
export interface RaceRegistrationsViewModel {
registeredDriverIds: string[];
count: number;
}
export interface IRaceRegistrationsPresenter {
present(registeredDriverIds: string[]): RaceRegistrationsViewModel;
getViewModel(): RaceRegistrationsViewModel;
}

View File

@@ -0,0 +1,34 @@
export interface RaceWithSOFViewModel {
id: string;
leagueId: string;
scheduledAt: string;
track: string;
trackId: string;
car: string;
carId: string;
sessionType: string;
status: string;
strengthOfField: number | null;
registeredCount: number;
maxParticipants: number;
participantCount: number;
}
export interface IRaceWithSOFPresenter {
present(
raceId: string,
leagueId: string,
scheduledAt: Date,
track: string,
trackId: string,
car: string,
carId: string,
sessionType: string,
status: string,
strengthOfField: number | null,
registeredCount: number,
maxParticipants: number,
participantCount: number
): RaceWithSOFViewModel;
getViewModel(): RaceWithSOFViewModel;
}

View File

@@ -0,0 +1,31 @@
export interface RaceListItemViewModel {
id: string;
track: string;
car: string;
scheduledAt: string;
status: 'scheduled' | 'running' | 'completed' | 'cancelled';
leagueId: string;
leagueName: string;
strengthOfField: number | null;
isUpcoming: boolean;
isLive: boolean;
isPast: boolean;
}
export interface RacesPageViewModel {
races: RaceListItemViewModel[];
stats: {
total: number;
scheduled: number;
running: number;
completed: number;
};
liveRaces: RaceListItemViewModel[];
upcomingThisWeek: RaceListItemViewModel[];
recentResults: RaceListItemViewModel[];
}
export interface IRacesPagePresenter {
present(races: any[]): void;
getViewModel(): RacesPageViewModel;
}

View File

@@ -0,0 +1,5 @@
import type { SponsoredLeagueDTO, SponsorDashboardDTO } from '../use-cases/GetSponsorDashboardQuery';
export interface ISponsorDashboardPresenter {
present(data: SponsorDashboardDTO | null): void;
}

View File

@@ -0,0 +1,5 @@
import type { SponsorSponsorshipsDTO } from '../use-cases/GetSponsorSponsorshipsQuery';
export interface ISponsorSponsorshipsPresenter {
present(data: SponsorSponsorshipsDTO | null): void;
}

View File

@@ -0,0 +1,29 @@
import type { Team, TeamMembership } from '../../domain/entities/Team';
export interface TeamDetailsViewModel {
team: {
id: string;
name: string;
tag: string;
description: string;
ownerId: string;
leagues: string[];
specialization?: 'endurance' | 'sprint' | 'mixed';
region?: string;
languages?: string[];
};
membership: {
role: 'owner' | 'manager' | 'member';
joinedAt: string;
isActive: boolean;
} | null;
canManage: boolean;
}
export interface ITeamDetailsPresenter {
present(
team: Team,
membership: TeamMembership | null,
driverId: string
): TeamDetailsViewModel;
}

View File

@@ -0,0 +1,25 @@
import type { TeamJoinRequest } from '../../domain/entities/Team';
export interface TeamJoinRequestViewModel {
requestId: string;
driverId: string;
driverName: string;
teamId: string;
status: 'pending' | 'approved' | 'rejected';
requestedAt: string;
avatarUrl: string;
}
export interface TeamJoinRequestsViewModel {
requests: TeamJoinRequestViewModel[];
pendingCount: number;
totalCount: number;
}
export interface ITeamJoinRequestsPresenter {
present(
requests: TeamJoinRequest[],
driverNames: Record<string, string>,
avatarUrls: Record<string, string>
): TeamJoinRequestsViewModel;
}

View File

@@ -0,0 +1,26 @@
import type { TeamMembership } from '../../domain/entities/Team';
export interface TeamMemberViewModel {
driverId: string;
driverName: string;
role: 'owner' | 'manager' | 'member';
joinedAt: string;
isActive: boolean;
avatarUrl: string;
}
export interface TeamMembersViewModel {
members: TeamMemberViewModel[];
totalCount: number;
ownerCount: number;
managerCount: number;
memberCount: number;
}
export interface ITeamMembersPresenter {
present(
memberships: TeamMembership[],
driverNames: Record<string, string>,
avatarUrls: Record<string, string>
): TeamMembersViewModel;
}

View File

@@ -0,0 +1,27 @@
export type SkillLevel = 'beginner' | 'intermediate' | 'advanced' | 'pro';
export interface TeamLeaderboardItemViewModel {
id: string;
name: string;
memberCount: number;
rating: number | null;
totalWins: number;
totalRaces: number;
performanceLevel: SkillLevel;
isRecruiting: boolean;
createdAt: Date;
description?: string;
specialization?: 'endurance' | 'sprint' | 'mixed';
region?: string;
languages?: string[];
}
export interface TeamsLeaderboardViewModel {
teams: TeamLeaderboardItemViewModel[];
recruitingCount: number;
}
export interface ITeamsLeaderboardPresenter {
present(teams: any[], recruitingCount: number): void;
getViewModel(): TeamsLeaderboardViewModel;
}

View File

@@ -3,23 +3,14 @@ import type { ILeagueMembershipRepository } from '../../domain/repositories/ILea
import type { ISeasonRepository } from '../../domain/repositories/ISeasonRepository';
import type { ILeagueScoringConfigRepository } from '../../domain/repositories/ILeagueScoringConfigRepository';
import type { IGameRepository } from '../../domain/repositories/IGameRepository';
import type {
LeagueScoringPresetProvider,
LeagueScoringPresetDTO,
} from '../ports/LeagueScoringPresetProvider';
import type {
LeagueSummaryDTO,
LeagueSummaryScoringDTO,
} from '../dto/LeagueSummaryDTO';
import type { LeagueScoringPresetProvider } from '../ports/LeagueScoringPresetProvider';
import type { IAllLeaguesWithCapacityAndScoringPresenter, LeagueEnrichedData } from '../presenters/IAllLeaguesWithCapacityAndScoringPresenter';
/**
* Combined capacity + scoring summary query for leagues.
*
* Extends the behavior of GetAllLeaguesWithCapacityQuery by including
* scoring preset and game summaries when an active season and
* LeagueScoringConfig are available.
* Use Case for retrieving all leagues with capacity and scoring information.
* Orchestrates domain logic and delegates presentation to the presenter.
*/
export class GetAllLeaguesWithCapacityAndScoringQuery {
export class GetAllLeaguesWithCapacityAndScoringUseCase {
constructor(
private readonly leagueRepository: ILeagueRepository,
private readonly leagueMembershipRepository: ILeagueMembershipRepository,
@@ -27,17 +18,16 @@ export class GetAllLeaguesWithCapacityAndScoringQuery {
private readonly leagueScoringConfigRepository: ILeagueScoringConfigRepository,
private readonly gameRepository: IGameRepository,
private readonly presetProvider: LeagueScoringPresetProvider,
public readonly presenter: IAllLeaguesWithCapacityAndScoringPresenter,
) {}
async execute(): Promise<LeagueSummaryDTO[]> {
async execute(): Promise<void> {
const leagues = await this.leagueRepository.findAll();
const results: LeagueSummaryDTO[] = [];
const enrichedLeagues: LeagueEnrichedData[] = [];
for (const league of leagues) {
const members = await this.leagueMembershipRepository.getLeagueMembers(
league.id,
);
const members = await this.leagueMembershipRepository.getLeagueMembers(league.id);
const usedDriverSlots = members.filter(
(m) =>
@@ -48,116 +38,36 @@ export class GetAllLeaguesWithCapacityAndScoringQuery {
m.role === 'member'),
).length;
const configuredMaxDrivers = league.settings.maxDrivers ?? usedDriverSlots;
const safeMaxDrivers = Math.max(configuredMaxDrivers, usedDriverSlots);
const seasons = await this.seasonRepository.findByLeagueId(league.id);
const activeSeason = seasons && seasons.length > 0
? seasons.find((s) => s.status === 'active') ?? seasons[0]
: undefined;
const scoringSummary = await this.buildScoringSummary(league.id);
let scoringConfig;
let game;
let preset;
const structureSummary = `Solo • ${safeMaxDrivers} drivers`;
if (activeSeason) {
scoringConfig = await this.leagueScoringConfigRepository.findBySeasonId(activeSeason.id);
if (scoringConfig) {
game = await this.gameRepository.findById(activeSeason.gameId);
const presetId = scoringConfig.scoringPresetId;
if (presetId) {
preset = this.presetProvider.getPresetById(presetId);
}
}
}
const qualifyingMinutes = 30;
const mainRaceMinutes =
typeof league.settings.sessionDuration === 'number'
? league.settings.sessionDuration
: 40;
const timingSummary = `${qualifyingMinutes} min Quali • ${mainRaceMinutes} min Race`;
const dto: LeagueSummaryDTO = {
id: league.id,
name: league.name,
description: league.description,
ownerId: league.ownerId,
createdAt: league.createdAt,
maxDrivers: safeMaxDrivers,
enrichedLeagues.push({
league,
usedDriverSlots,
maxTeams: undefined,
usedTeamSlots: undefined,
structureSummary,
scoringPatternSummary: scoringSummary?.scoringPatternSummary,
timingSummary,
scoring: scoringSummary,
};
results.push(dto);
season: activeSeason,
scoringConfig,
game,
preset,
});
}
return results;
}
private async buildScoringSummary(
leagueId: string,
): Promise<LeagueSummaryScoringDTO | undefined> {
const seasons = await this.seasonRepository.findByLeagueId(leagueId);
if (!seasons || seasons.length === 0) {
return undefined;
}
const activeSeason =
seasons.find((s) => s.status === 'active') ?? seasons[0];
const scoringConfig =
await this.leagueScoringConfigRepository.findBySeasonId(activeSeason.id);
if (!scoringConfig) {
return undefined;
}
const game = await this.gameRepository.findById(activeSeason.gameId);
if (!game) {
return undefined;
}
const presetId = scoringConfig.scoringPresetId;
let preset: LeagueScoringPresetDTO | undefined;
if (presetId) {
preset = this.presetProvider.getPresetById(presetId);
}
const dropPolicySummary =
preset?.dropPolicySummary ?? this.deriveDropPolicySummary(scoringConfig);
const primaryChampionshipType =
preset?.primaryChampionshipType ??
(scoringConfig.championships[0]?.type ?? 'driver');
const scoringPresetName = preset?.name ?? 'Custom';
const scoringPatternSummary = `${scoringPresetName}${dropPolicySummary}`;
return {
gameId: game.id,
gameName: game.name,
primaryChampionshipType,
scoringPresetId: presetId ?? 'custom',
scoringPresetName,
dropPolicySummary,
scoringPatternSummary,
};
}
private deriveDropPolicySummary(config: {
championships: Array<{
dropScorePolicy: { strategy: string; count?: number; dropCount?: number };
}>;
}): string {
const championship = config.championships[0];
if (!championship) {
return 'All results count';
}
const policy = championship.dropScorePolicy;
if (!policy || policy.strategy === 'none') {
return 'All results count';
}
if (policy.strategy === 'bestNResults' && typeof policy.count === 'number') {
return `Best ${policy.count} results count`;
}
if (
policy.strategy === 'dropWorstN' &&
typeof policy.dropCount === 'number'
) {
return `Worst ${policy.dropCount} results are dropped`;
}
return 'Custom drop score rules';
this.presenter.present(enrichedLeagues);
}
}

View File

@@ -1,17 +1,22 @@
import type { ILeagueRepository } from '../../domain/repositories/ILeagueRepository';
import type { ILeagueMembershipRepository } from '../../domain/repositories/ILeagueMembershipRepository';
import type { LeagueDTO } from '../dto/LeagueDTO';
import type { IAllLeaguesWithCapacityPresenter } from '../presenters/IAllLeaguesWithCapacityPresenter';
export class GetAllLeaguesWithCapacityQuery {
/**
* Use Case for retrieving all leagues with capacity information.
* Orchestrates domain logic and delegates presentation to the presenter.
*/
export class GetAllLeaguesWithCapacityUseCase {
constructor(
private readonly leagueRepository: ILeagueRepository,
private readonly leagueMembershipRepository: ILeagueMembershipRepository,
public readonly presenter: IAllLeaguesWithCapacityPresenter,
) {}
async execute(): Promise<LeagueDTO[]> {
async execute(): Promise<void> {
const leagues = await this.leagueRepository.findAll();
const results: LeagueDTO[] = [];
const memberCounts = new Map<string, number>();
for (const league of leagues) {
const members = await this.leagueMembershipRepository.getLeagueMembers(league.id);
@@ -25,34 +30,9 @@ export class GetAllLeaguesWithCapacityQuery {
m.role === 'member'),
).length;
// Ensure we never expose an impossible state like 26/24:
// clamp maxDrivers to at least usedSlots at the application boundary.
const configuredMax = league.settings.maxDrivers ?? usedSlots;
const safeMaxDrivers = Math.max(configuredMax, usedSlots);
const dto: LeagueDTO = {
id: league.id,
name: league.name,
description: league.description,
ownerId: league.ownerId,
settings: {
...league.settings,
maxDrivers: safeMaxDrivers,
},
createdAt: league.createdAt.toISOString(),
socialLinks: league.socialLinks
? {
discordUrl: league.socialLinks.discordUrl,
youtubeUrl: league.socialLinks.youtubeUrl,
websiteUrl: league.socialLinks.websiteUrl,
}
: undefined,
usedSlots,
};
results.push(dto);
memberCounts.set(league.id, usedSlots);
}
return results;
this.presenter.present(leagues, memberCounts);
}
}

View File

@@ -1,13 +1,32 @@
import type { ITeamRepository } from '../../domain/repositories/ITeamRepository';
import type { GetAllTeamsQueryResultDTO } from '../dto/TeamCommandAndQueryDTO';
import type { ITeamMembershipRepository } from '../../domain/repositories/ITeamMembershipRepository';
import type { IAllTeamsPresenter } from '../presenters/IAllTeamsPresenter';
export class GetAllTeamsQuery {
/**
* Use Case for retrieving all teams.
* Orchestrates domain logic and delegates presentation to the presenter.
*/
export class GetAllTeamsUseCase {
constructor(
private readonly teamRepository: ITeamRepository,
private readonly teamMembershipRepository: ITeamMembershipRepository,
public readonly presenter: IAllTeamsPresenter,
) {}
async execute(): Promise<GetAllTeamsQueryResultDTO> {
async execute(): Promise<void> {
const teams = await this.teamRepository.findAll();
return teams;
// Enrich teams with member counts
const enrichedTeams = await Promise.all(
teams.map(async (team) => {
const memberships = await this.teamMembershipRepository.findByTeamId(team.id);
return {
...team,
memberCount: memberships.length,
};
})
);
this.presenter.present(enrichedTeams as any);
}
}

View File

@@ -1,29 +1,30 @@
import type { ITeamRepository } from '../../domain/repositories/ITeamRepository';
import type { ITeamMembershipRepository } from '../../domain/repositories/ITeamMembershipRepository';
import type {
GetDriverTeamQueryParamsDTO,
GetDriverTeamQueryResultDTO,
} from '../dto/TeamCommandAndQueryDTO';
import type { IDriverTeamPresenter } from '../presenters/IDriverTeamPresenter';
export class GetDriverTeamQuery {
/**
* Use Case for retrieving a driver's team.
* Orchestrates domain logic and delegates presentation to the presenter.
*/
export class GetDriverTeamUseCase {
constructor(
private readonly teamRepository: ITeamRepository,
private readonly membershipRepository: ITeamMembershipRepository,
public readonly presenter: IDriverTeamPresenter,
) {}
async execute(params: GetDriverTeamQueryParamsDTO): Promise<GetDriverTeamQueryResultDTO | null> {
const { driverId } = params;
async execute(driverId: string): Promise<boolean> {
const membership = await this.membershipRepository.getActiveMembershipForDriver(driverId);
if (!membership) {
return null;
return false;
}
const team = await this.teamRepository.findById(membership.teamId);
if (!team) {
return null;
return false;
}
return { team, membership };
this.presenter.present(team, membership, driverId);
return true;
}
}

View File

@@ -0,0 +1,37 @@
import type { IDriverRepository } from '../../domain/repositories/IDriverRepository';
import type { IRankingService } from '../../domain/services/IRankingService';
import type { IDriverStatsService } from '../../domain/services/IDriverStatsService';
import type { IImageService } from '../../domain/services/IImageService';
import type { IDriversLeaderboardPresenter } from '../presenters/IDriversLeaderboardPresenter';
/**
* Use Case for retrieving driver leaderboard data.
* Orchestrates domain logic and delegates presentation to the presenter.
*/
export class GetDriversLeaderboardUseCase {
constructor(
private readonly driverRepository: IDriverRepository,
private readonly rankingService: IRankingService,
private readonly driverStatsService: IDriverStatsService,
private readonly imageService: IImageService,
public readonly presenter: IDriversLeaderboardPresenter,
) {}
async execute(): Promise<void> {
const drivers = await this.driverRepository.findAll();
const rankings = this.rankingService.getAllDriverRankings();
const stats: Record<string, any> = {};
const avatarUrls: Record<string, string> = {};
for (const driver of drivers) {
const driverStats = this.driverStatsService.getDriverStats(driver.id);
if (driverStats) {
stats[driver.id] = driverStats;
}
avatarUrls[driver.id] = this.imageService.getDriverAvatar(driver.id);
}
this.presenter.present(drivers, rankings, stats, avatarUrls);
}
}

View File

@@ -1,6 +1,6 @@
/**
* Query: GetEntitySponsorshipPricingQuery
*
* Application Use Case: GetEntitySponsorshipPricingUseCase
*
* Retrieves sponsorship pricing configuration for any entity.
* Used by sponsors to see available slots and prices.
*/
@@ -10,6 +10,7 @@ import type { ISponsorshipRequestRepository } from '../../domain/repositories/IS
import type { ISeasonSponsorshipRepository } from '../../domain/repositories/ISeasonSponsorshipRepository';
import type { SponsorableEntityType } from '../../domain/entities/SponsorshipRequest';
import type { SponsorshipTier } from '../../domain/entities/SeasonSponsorship';
import type { IEntitySponsorshipPricingPresenter } from '../presenters/IEntitySponsorshipPricingPresenter';
export interface GetEntitySponsorshipPricingDTO {
entityType: SponsorableEntityType;
@@ -37,18 +38,20 @@ export interface GetEntitySponsorshipPricingResultDTO {
secondarySlot?: SponsorshipSlotDTO;
}
export class GetEntitySponsorshipPricingQuery {
export class GetEntitySponsorshipPricingUseCase {
constructor(
private readonly sponsorshipPricingRepo: ISponsorshipPricingRepository,
private readonly sponsorshipRequestRepo: ISponsorshipRequestRepository,
private readonly seasonSponsorshipRepo: ISeasonSponsorshipRepository,
private readonly presenter: IEntitySponsorshipPricingPresenter,
) {}
async execute(dto: GetEntitySponsorshipPricingDTO): Promise<GetEntitySponsorshipPricingResultDTO | null> {
async execute(dto: GetEntitySponsorshipPricingDTO): Promise<void> {
const pricing = await this.sponsorshipPricingRepo.findByEntity(dto.entityType, dto.entityId);
if (!pricing) {
return null;
this.presenter.present(null);
return;
}
// Count pending requests by tier
@@ -107,6 +110,6 @@ export class GetEntitySponsorshipPricingQuery {
};
}
return result;
this.presenter.present(result);
}
}

View File

@@ -2,26 +2,31 @@ import type { IStandingRepository } from '../../domain/repositories/IStandingRep
import type { IResultRepository } from '../../domain/repositories/IResultRepository';
import type { IPenaltyRepository } from '../../domain/repositories/IPenaltyRepository';
import type { IRaceRepository } from '../../domain/repositories/IRaceRepository';
import type { LeagueDriverSeasonStatsDTO } from '../dto/LeagueDriverSeasonStatsDTO';
import type { ILeagueDriverSeasonStatsPresenter } from '../presenters/ILeagueDriverSeasonStatsPresenter';
export interface DriverRatingPort {
getRating(driverId: string): { rating: number | null; ratingChange: number | null };
}
export interface GetLeagueDriverSeasonStatsQueryParamsDTO {
export interface GetLeagueDriverSeasonStatsUseCaseParams {
leagueId: string;
}
export class GetLeagueDriverSeasonStatsQuery {
/**
* Use Case for retrieving league driver season statistics.
* Orchestrates domain logic and delegates presentation to the presenter.
*/
export class GetLeagueDriverSeasonStatsUseCase {
constructor(
private readonly standingRepository: IStandingRepository,
private readonly resultRepository: IResultRepository,
private readonly penaltyRepository: IPenaltyRepository,
private readonly raceRepository: IRaceRepository,
private readonly driverRatingPort: DriverRatingPort,
public readonly presenter: ILeagueDriverSeasonStatsPresenter,
) {}
async execute(params: GetLeagueDriverSeasonStatsQueryParamsDTO): Promise<LeagueDriverSeasonStatsDTO[]> {
async execute(params: GetLeagueDriverSeasonStatsUseCaseParams): Promise<void> {
const { leagueId } = params;
// Get standings and races for the league
@@ -53,59 +58,26 @@ export class GetLeagueDriverSeasonStatsQuery {
penaltiesByDriver.set(p.driverId, current);
}
// Build basic stats per driver from standings
const statsByDriver = new Map<string, LeagueDriverSeasonStatsDTO>();
// Collect driver ratings
const driverRatings = new Map<string, { rating: number | null; ratingChange: number | null }>();
for (const standing of standings) {
const penalty = penaltiesByDriver.get(standing.driverId) ?? { baseDelta: 0, bonusDelta: 0 };
const totalPenaltyPoints = penalty.baseDelta;
const bonusPoints = penalty.bonusDelta;
const racesCompleted = standing.racesCompleted;
const pointsPerRace = racesCompleted > 0 ? standing.points / racesCompleted : 0;
const ratingInfo = this.driverRatingPort.getRating(standing.driverId);
const dto: LeagueDriverSeasonStatsDTO = {
leagueId,
driverId: standing.driverId,
position: standing.position,
driverName: '',
teamId: undefined,
teamName: undefined,
totalPoints: standing.points + totalPenaltyPoints + bonusPoints,
basePoints: standing.points,
penaltyPoints: Math.abs(totalPenaltyPoints),
bonusPoints,
pointsPerRace,
racesStarted: racesCompleted,
racesFinished: racesCompleted,
dnfs: 0,
noShows: 0,
avgFinish: null,
rating: ratingInfo.rating,
ratingChange: ratingInfo.ratingChange,
};
statsByDriver.set(standing.driverId, dto);
driverRatings.set(standing.driverId, ratingInfo);
}
// Enhance stats with basic finish-position-based avgFinish from results
for (const [driverId, dto] of statsByDriver.entries()) {
const driverResults = await this.resultRepository.findByDriverIdAndLeagueId(driverId, leagueId);
if (driverResults.length > 0) {
const totalPositions = driverResults.reduce((sum, r) => sum + r.position, 0);
const avgFinish = totalPositions / driverResults.length;
dto.avgFinish = Number.isFinite(avgFinish) ? Number(avgFinish.toFixed(2)) : null;
dto.racesStarted = driverResults.length;
dto.racesFinished = driverResults.length;
}
statsByDriver.set(driverId, dto);
// Collect driver results
const driverResults = new Map<string, Array<{ position: number }>>();
for (const standing of standings) {
const results = await this.resultRepository.findByDriverIdAndLeagueId(standing.driverId, leagueId);
driverResults.set(standing.driverId, results);
}
// Ensure ordering by position
const result = Array.from(statsByDriver.values()).sort((a, b) => a.position - b.position);
return result;
this.presenter.present(
leagueId,
standings,
penaltiesByDriver,
driverResults,
driverRatings
);
}
}

View File

@@ -2,153 +2,52 @@ import type { ILeagueRepository } from '../../domain/repositories/ILeagueReposit
import type { ISeasonRepository } from '../../domain/repositories/ISeasonRepository';
import type { ILeagueScoringConfigRepository } from '../../domain/repositories/ILeagueScoringConfigRepository';
import type { IGameRepository } from '../../domain/repositories/IGameRepository';
import type { ChampionshipConfig } from '../../domain/value-objects/ChampionshipConfig';
import type { DropScorePolicy } from '../../domain/value-objects/DropScorePolicy';
import type {
LeagueConfigFormModel,
LeagueDropPolicyFormDTO,
} from '../dto/LeagueConfigFormDTO';
import type { ILeagueFullConfigPresenter, LeagueFullConfigData } from '../presenters/ILeagueFullConfigPresenter';
/**
* Query returning a unified LeagueConfigFormModel for a given league.
*
* First iteration focuses on:
* - Basics derived from League
* - Simple solo structure derived from League.settings.maxDrivers
* - Championships flags with driver enabled and others disabled
* - Scoring pattern id taken from LeagueScoringConfig.scoringPresetId
* - Drop policy inferred from the primary championship configuration
* Use Case for retrieving a league's full configuration.
* Orchestrates domain logic and delegates presentation to the presenter.
*/
export class GetLeagueFullConfigQuery {
export class GetLeagueFullConfigUseCase {
constructor(
private readonly leagueRepository: ILeagueRepository,
private readonly seasonRepository: ISeasonRepository,
private readonly leagueScoringConfigRepository: ILeagueScoringConfigRepository,
private readonly gameRepository: IGameRepository,
public readonly presenter: ILeagueFullConfigPresenter,
) {}
async execute(params: { leagueId: string }): Promise<LeagueConfigFormModel | null> {
async execute(params: { leagueId: string }): Promise<void> {
const { leagueId } = params;
const league = await this.leagueRepository.findById(leagueId);
if (!league) {
return null;
throw new Error(`League ${leagueId} not found`);
}
const seasons = await this.seasonRepository.findByLeagueId(leagueId);
const activeSeason =
seasons && seasons.length > 0
? seasons.find((s) => s.status === 'active') ?? seasons[0]
: null;
const scoringConfig = activeSeason
? await this.leagueScoringConfigRepository.findBySeasonId(activeSeason.id)
: null;
const game =
activeSeason && activeSeason.gameId
? await this.gameRepository.findById(activeSeason.gameId)
: null;
const patternId = scoringConfig?.scoringPresetId;
const primaryChampionship: ChampionshipConfig | undefined =
scoringConfig && scoringConfig.championships && scoringConfig.championships.length > 0
? scoringConfig.championships[0]
: undefined;
const dropPolicy: DropScorePolicy | undefined =
primaryChampionship?.dropScorePolicy ?? undefined;
let scoringConfig;
let game;
const dropPolicyForm: LeagueDropPolicyFormDTO = this.mapDropPolicy(dropPolicy);
const defaultQualifyingMinutes = 30;
const defaultMainRaceMinutes = 40;
const mainRaceMinutes =
typeof league.settings.sessionDuration === 'number'
? league.settings.sessionDuration
: defaultMainRaceMinutes;
const qualifyingMinutes = defaultQualifyingMinutes;
const roundsPlanned = 8;
let sessionCount = 2;
if (
primaryChampionship &&
Array.isArray((primaryChampionship as any).sessionTypes) &&
(primaryChampionship as any).sessionTypes.length > 0
) {
sessionCount = (primaryChampionship as any).sessionTypes.length;
if (activeSeason) {
scoringConfig = await this.leagueScoringConfigRepository.findBySeasonId(activeSeason.id);
if (activeSeason.gameId) {
game = await this.gameRepository.findById(activeSeason.gameId);
}
}
const practiceMinutes = 20;
const sprintRaceMinutes = patternId === 'sprint-main-driver' ? 20 : undefined;
const form: LeagueConfigFormModel = {
leagueId: league.id,
basics: {
name: league.name,
description: league.description,
visibility: 'public', // current domain model does not track visibility; default to public for now
gameId: game?.id ?? 'iracing',
},
structure: {
// First slice: treat everything as solo structure based on maxDrivers
mode: 'solo',
maxDrivers: league.settings.maxDrivers ?? 32,
maxTeams: undefined,
driversPerTeam: undefined,
multiClassEnabled: false,
},
championships: {
enableDriverChampionship: true,
enableTeamChampionship: false,
enableNationsChampionship: false,
enableTrophyChampionship: false,
},
scoring: {
patternId: patternId ?? undefined,
customScoringEnabled: !patternId,
},
dropPolicy: dropPolicyForm,
timings: {
practiceMinutes,
qualifyingMinutes,
sprintRaceMinutes,
mainRaceMinutes,
sessionCount,
roundsPlanned,
},
stewarding: {
decisionMode: 'admin_only',
requireDefense: true,
defenseTimeLimit: 48,
voteTimeLimit: 72,
protestDeadlineHours: 72,
stewardingClosesHours: 168,
notifyAccusedOnProtest: true,
notifyOnVoteRequired: true,
},
const data: LeagueFullConfigData = {
league,
activeSeason,
scoringConfig,
game,
};
return form;
}
private mapDropPolicy(policy: DropScorePolicy | undefined): LeagueDropPolicyFormDTO {
if (!policy || policy.strategy === 'none') {
return { strategy: 'none' };
}
if (policy.strategy === 'bestNResults') {
const n = typeof policy.count === 'number' ? policy.count : undefined;
return n !== undefined ? { strategy: 'bestNResults', n } : { strategy: 'none' };
}
if (policy.strategy === 'dropWorstN') {
const n = typeof policy.dropCount === 'number' ? policy.dropCount : undefined;
return n !== undefined ? { strategy: 'dropWorstN', n } : { strategy: 'none' };
}
return { strategy: 'none' };
this.presenter.present(data);
}
}

View File

@@ -2,41 +2,34 @@ import type { ILeagueRepository } from '../../domain/repositories/ILeagueReposit
import type { ISeasonRepository } from '../../domain/repositories/ISeasonRepository';
import type { ILeagueScoringConfigRepository } from '../../domain/repositories/ILeagueScoringConfigRepository';
import type { IGameRepository } from '../../domain/repositories/IGameRepository';
import type { LeagueScoringConfigDTO } from '../dto/LeagueScoringConfigDTO';
import type { LeagueScoringChampionshipDTO } from '../dto/LeagueScoringConfigDTO';
import type {
LeagueScoringPresetProvider,
LeagueScoringPresetDTO,
} from '../ports/LeagueScoringPresetProvider';
import type { ChampionshipConfig } from '../../domain/value-objects/ChampionshipConfig';
import type { PointsTable } from '../../domain/value-objects/PointsTable';
import type { BonusRule } from '../../domain/value-objects/BonusRule';
import type { LeagueScoringPresetProvider } from '../ports/LeagueScoringPresetProvider';
import type { ILeagueScoringConfigPresenter, LeagueScoringConfigData } from '../presenters/ILeagueScoringConfigPresenter';
/**
* Query returning a league's scoring configuration for its active season.
*
* Designed for the league detail "Scoring" tab.
* Use Case for retrieving a league's scoring configuration for its active season.
* Orchestrates domain logic and delegates presentation to the presenter.
*/
export class GetLeagueScoringConfigQuery {
export class GetLeagueScoringConfigUseCase {
constructor(
private readonly leagueRepository: ILeagueRepository,
private readonly seasonRepository: ISeasonRepository,
private readonly leagueScoringConfigRepository: ILeagueScoringConfigRepository,
private readonly gameRepository: IGameRepository,
private readonly presetProvider: LeagueScoringPresetProvider,
public readonly presenter: ILeagueScoringConfigPresenter,
) {}
async execute(params: { leagueId: string }): Promise<LeagueScoringConfigDTO | null> {
async execute(params: { leagueId: string }): Promise<void> {
const { leagueId } = params;
const league = await this.leagueRepository.findById(leagueId);
if (!league) {
return null;
throw new Error(`League ${leagueId} not found`);
}
const seasons = await this.seasonRepository.findByLeagueId(leagueId);
if (!seasons || seasons.length === 0) {
return null;
throw new Error(`No seasons found for league ${leagueId}`);
}
const activeSeason =
@@ -45,146 +38,27 @@ export class GetLeagueScoringConfigQuery {
const scoringConfig =
await this.leagueScoringConfigRepository.findBySeasonId(activeSeason.id);
if (!scoringConfig) {
return null;
throw new Error(`No scoring config found for season ${activeSeason.id}`);
}
const game = await this.gameRepository.findById(activeSeason.gameId);
if (!game) {
return null;
throw new Error(`Game ${activeSeason.gameId} not found`);
}
const presetId = scoringConfig.scoringPresetId;
const preset: LeagueScoringPresetDTO | undefined =
presetId ? this.presetProvider.getPresetById(presetId) : undefined;
const preset = presetId ? this.presetProvider.getPresetById(presetId) : undefined;
const championships: LeagueScoringChampionshipDTO[] =
scoringConfig.championships.map((champ) =>
this.mapChampionship(champ),
);
const dropPolicySummary =
preset?.dropPolicySummary ??
this.deriveDropPolicyDescriptionFromChampionships(
scoringConfig.championships,
);
return {
const data: LeagueScoringConfigData = {
leagueId: league.id,
seasonId: activeSeason.id,
gameId: game.id,
gameName: game.name,
scoringPresetId: presetId,
scoringPresetName: preset?.name,
dropPolicySummary,
championships,
preset,
championships: scoringConfig.championships,
};
}
private mapChampionship(championship: ChampionshipConfig): LeagueScoringChampionshipDTO {
const sessionTypes = championship.sessionTypes.map((s) => s.toString());
const pointsPreview = this.buildPointsPreview(championship.pointsTableBySessionType);
const bonusSummary = this.buildBonusSummary(
championship.bonusRulesBySessionType ?? {},
);
const dropPolicyDescription = this.deriveDropPolicyDescription(
championship.dropScorePolicy,
);
return {
id: championship.id,
name: championship.name,
type: championship.type,
sessionTypes,
pointsPreview,
bonusSummary,
dropPolicyDescription,
};
}
private buildPointsPreview(
tables: Record<string, any>,
): Array<{ sessionType: string; position: number; points: number }> {
const preview: Array<{
sessionType: string;
position: number;
points: number;
}> = [];
const maxPositions = 10;
for (const [sessionType, table] of Object.entries(tables)) {
for (let pos = 1; pos <= maxPositions; pos++) {
const points = table.getPointsForPosition(pos);
if (points && points !== 0) {
preview.push({
sessionType,
position: pos,
points,
});
}
}
}
return preview;
}
private buildBonusSummary(
bonusRulesBySessionType: Record<string, BonusRule[]>,
): string[] {
const summaries: string[] = [];
for (const [sessionType, rules] of Object.entries(bonusRulesBySessionType)) {
for (const rule of rules) {
if (rule.type === 'fastestLap') {
const base = `Fastest lap in ${sessionType}`;
if (rule.requiresFinishInTopN) {
summaries.push(
`${base} +${rule.points} points if finishing P${rule.requiresFinishInTopN} or better`,
);
} else {
summaries.push(`${base} +${rule.points} points`);
}
} else {
summaries.push(
`${rule.type} bonus in ${sessionType} worth ${rule.points} points`,
);
}
}
}
return summaries;
}
private deriveDropPolicyDescriptionFromChampionships(
championships: ChampionshipConfig[],
): string {
const first = championships[0];
if (!first) {
return 'All results count';
}
return this.deriveDropPolicyDescription(first.dropScorePolicy);
}
private deriveDropPolicyDescription(policy: {
strategy: string;
count?: number;
dropCount?: number;
}): string {
if (!policy || policy.strategy === 'none') {
return 'All results count';
}
if (policy.strategy === 'bestNResults' && typeof policy.count === 'number') {
return `Best ${policy.count} results count towards the championship`;
}
if (
policy.strategy === 'dropWorstN' &&
typeof policy.dropCount === 'number'
) {
return `Worst ${policy.dropCount} results are dropped from the championship total`;
}
return 'Custom drop score rules apply';
this.presenter.present(data);
}
}

View File

@@ -1,18 +1,22 @@
import type { IStandingRepository } from '../../domain/repositories/IStandingRepository';
import type { StandingDTO } from '../dto/StandingDTO';
import { EntityMappers } from '../mappers/EntityMappers';
import type { ILeagueStandingsPresenter } from '../presenters/ILeagueStandingsPresenter';
export interface GetLeagueStandingsQueryParamsDTO {
export interface GetLeagueStandingsUseCaseParams {
leagueId: string;
}
export class GetLeagueStandingsQuery {
/**
* Use Case for retrieving league standings.
* Orchestrates domain logic and delegates presentation to the presenter.
*/
export class GetLeagueStandingsUseCase {
constructor(
private readonly standingRepository: IStandingRepository,
public readonly presenter: ILeagueStandingsPresenter,
) {}
async execute(params: GetLeagueStandingsQueryParamsDTO): Promise<StandingDTO[]> {
async execute(params: GetLeagueStandingsUseCaseParams): Promise<void> {
const standings = await this.standingRepository.findByLeagueId(params.leagueId);
return EntityMappers.toStandingDTOs(standings);
this.presenter.present(standings);
}
}

View File

@@ -1,33 +1,26 @@
/**
* Application Query: GetLeagueStatsQuery
*
* Returns league statistics including average SOF across completed races.
* Use Case for retrieving league statistics.
* Orchestrates domain logic and delegates presentation to the presenter.
*/
import type { ILeagueRepository } from '../../domain/repositories/ILeagueRepository';
import type { IRaceRepository } from '../../domain/repositories/IRaceRepository';
import type { IResultRepository } from '../../domain/repositories/IResultRepository';
import type { DriverRatingProvider } from '../ports/DriverRatingProvider';
import type { ILeagueStatsPresenter } from '../presenters/ILeagueStatsPresenter';
import {
AverageStrengthOfFieldCalculator,
type StrengthOfFieldCalculator,
} from '../../domain/services/StrengthOfFieldCalculator';
export interface GetLeagueStatsQueryParams {
export interface GetLeagueStatsUseCaseParams {
leagueId: string;
}
export interface LeagueStatsDTO {
leagueId: string;
totalRaces: number;
completedRaces: number;
scheduledRaces: number;
averageSOF: number | null;
highestSOF: number | null;
lowestSOF: number | null;
}
export class GetLeagueStatsQuery {
/**
* Use Case for retrieving league statistics including average SOF across completed races.
*/
export class GetLeagueStatsUseCase {
private readonly sofCalculator: StrengthOfFieldCalculator;
constructor(
@@ -35,17 +28,18 @@ export class GetLeagueStatsQuery {
private readonly raceRepository: IRaceRepository,
private readonly resultRepository: IResultRepository,
private readonly driverRatingProvider: DriverRatingProvider,
public readonly presenter: ILeagueStatsPresenter,
sofCalculator?: StrengthOfFieldCalculator,
) {
this.sofCalculator = sofCalculator ?? new AverageStrengthOfFieldCalculator();
}
async execute(params: GetLeagueStatsQueryParams): Promise<LeagueStatsDTO | null> {
async execute(params: GetLeagueStatsUseCaseParams): Promise<void> {
const { leagueId } = params;
const league = await this.leagueRepository.findById(leagueId);
if (!league) {
return null;
throw new Error(`League ${leagueId} not found`);
}
const races = await this.raceRepository.findByLeagueId(leagueId);
@@ -78,22 +72,12 @@ export class GetLeagueStatsQuery {
}
}
// Calculate aggregate stats
const averageSOF = sofValues.length > 0
? Math.round(sofValues.reduce((a, b) => a + b, 0) / sofValues.length)
: null;
const highestSOF = sofValues.length > 0 ? Math.max(...sofValues) : null;
const lowestSOF = sofValues.length > 0 ? Math.min(...sofValues) : null;
return {
this.presenter.present(
leagueId,
totalRaces: races.length,
completedRaces: completedRaces.length,
scheduledRaces: scheduledRaces.length,
averageSOF,
highestSOF,
lowestSOF,
};
races.length,
completedRaces.length,
scheduledRaces.length,
sofValues
);
}
}

View File

@@ -1,6 +1,6 @@
/**
* Query: GetPendingSponsorshipRequestsQuery
*
* Application Use Case: GetPendingSponsorshipRequestsUseCase
*
* Retrieves pending sponsorship requests for an entity owner to review.
*/
@@ -8,6 +8,7 @@ import type { ISponsorshipRequestRepository } from '../../domain/repositories/IS
import type { ISponsorRepository } from '../../domain/repositories/ISponsorRepository';
import type { SponsorableEntityType } from '../../domain/entities/SponsorshipRequest';
import type { SponsorshipTier } from '../../domain/entities/SeasonSponsorship';
import type { IPendingSponsorshipRequestsPresenter } from '../presenters/IPendingSponsorshipRequestsPresenter';
export interface GetPendingSponsorshipRequestsDTO {
entityType: SponsorableEntityType;
@@ -36,13 +37,14 @@ export interface GetPendingSponsorshipRequestsResultDTO {
totalCount: number;
}
export class GetPendingSponsorshipRequestsQuery {
export class GetPendingSponsorshipRequestsUseCase {
constructor(
private readonly sponsorshipRequestRepo: ISponsorshipRequestRepository,
private readonly sponsorRepo: ISponsorRepository,
private readonly presenter: IPendingSponsorshipRequestsPresenter,
) {}
async execute(dto: GetPendingSponsorshipRequestsDTO): Promise<GetPendingSponsorshipRequestsResultDTO> {
async execute(dto: GetPendingSponsorshipRequestsDTO): Promise<void> {
const requests = await this.sponsorshipRequestRepo.findPendingByEntity(
dto.entityType,
dto.entityId
@@ -72,11 +74,11 @@ export class GetPendingSponsorshipRequestsQuery {
// Sort by creation date (newest first)
requestDTOs.sort((a, b) => b.createdAt.getTime() - a.createdAt.getTime());
return {
this.presenter.present({
entityType: dto.entityType,
entityId: dto.entityId,
requests: requestDTOs,
totalCount: requestDTOs.length,
};
});
}
}

View File

@@ -1,38 +1,22 @@
/**
* Application Query: GetRacePenaltiesQuery
*
* Use Case: GetRacePenaltiesUseCase
*
* Returns all penalties applied for a specific race, with driver details.
* Orchestrates domain logic and delegates presentation to the presenter.
*/
import type { IPenaltyRepository } from '../../domain/repositories/IPenaltyRepository';
import type { IDriverRepository } from '../../domain/repositories/IDriverRepository';
import type { PenaltyType, PenaltyStatus } from '../../domain/entities/Penalty';
import type { IRacePenaltiesPresenter } from '../presenters/IRacePenaltiesPresenter';
export interface RacePenaltyDTO {
id: string;
raceId: string;
driverId: string;
driverName: string;
type: PenaltyType;
value?: number;
reason: string;
protestId?: string;
issuedBy: string;
issuedByName: string;
status: PenaltyStatus;
description: string;
issuedAt: string;
appliedAt?: string;
notes?: string;
}
export class GetRacePenaltiesQuery {
export class GetRacePenaltiesUseCase {
constructor(
private readonly penaltyRepository: IPenaltyRepository,
private readonly driverRepository: IDriverRepository,
public readonly presenter: IRacePenaltiesPresenter,
) {}
async execute(raceId: string): Promise<RacePenaltyDTO[]> {
async execute(raceId: string): Promise<void> {
const penalties = await this.penaltyRepository.findByRaceId(raceId);
// Load all driver details in parallel
@@ -53,22 +37,6 @@ export class GetRacePenaltiesQuery {
}
});
return penalties.map(penalty => ({
id: penalty.id,
raceId: penalty.raceId,
driverId: penalty.driverId,
driverName: driverMap.get(penalty.driverId) || 'Unknown',
type: penalty.type,
value: penalty.value,
reason: penalty.reason,
protestId: penalty.protestId,
issuedBy: penalty.issuedBy,
issuedByName: driverMap.get(penalty.issuedBy) || 'Unknown',
status: penalty.status,
description: penalty.getDescription(),
issuedAt: penalty.issuedAt.toISOString(),
appliedAt: penalty.appliedAt?.toISOString(),
notes: penalty.notes,
}));
this.presenter.present(penalties, driverMap);
}
}

View File

@@ -1,38 +1,22 @@
/**
* Application Query: GetRaceProtestsQuery
*
* Use Case: GetRaceProtestsUseCase
*
* Returns all protests filed for a specific race, with driver details.
* Orchestrates domain logic and delegates presentation to the presenter.
*/
import type { IProtestRepository } from '../../domain/repositories/IProtestRepository';
import type { IDriverRepository } from '../../domain/repositories/IDriverRepository';
import type { ProtestStatus, ProtestIncident } from '../../domain/entities/Protest';
import type { IRaceProtestsPresenter } from '../presenters/IRaceProtestsPresenter';
export interface RaceProtestDTO {
id: string;
raceId: string;
protestingDriverId: string;
protestingDriverName: string;
accusedDriverId: string;
accusedDriverName: string;
incident: ProtestIncident;
comment?: string;
proofVideoUrl?: string;
status: ProtestStatus;
reviewedBy?: string;
reviewedByName?: string;
decisionNotes?: string;
filedAt: string;
reviewedAt?: string;
}
export class GetRaceProtestsQuery {
export class GetRaceProtestsUseCase {
constructor(
private readonly protestRepository: IProtestRepository,
private readonly driverRepository: IDriverRepository,
public readonly presenter: IRaceProtestsPresenter,
) {}
async execute(raceId: string): Promise<RaceProtestDTO[]> {
async execute(raceId: string): Promise<void> {
const protests = await this.protestRepository.findByRaceId(raceId);
// Load all driver details in parallel
@@ -56,22 +40,6 @@ export class GetRaceProtestsQuery {
}
});
return protests.map(protest => ({
id: protest.id,
raceId: protest.raceId,
protestingDriverId: protest.protestingDriverId,
protestingDriverName: driverMap.get(protest.protestingDriverId) || 'Unknown',
accusedDriverId: protest.accusedDriverId,
accusedDriverName: driverMap.get(protest.accusedDriverId) || 'Unknown',
incident: protest.incident,
comment: protest.comment,
proofVideoUrl: protest.proofVideoUrl,
status: protest.status,
reviewedBy: protest.reviewedBy,
reviewedByName: protest.reviewedBy ? driverMap.get(protest.reviewedBy) : undefined,
decisionNotes: protest.decisionNotes,
filedAt: protest.filedAt.toISOString(),
reviewedAt: protest.reviewedAt?.toISOString(),
}));
this.presenter.present(protests, driverMap);
}
}

View File

@@ -1,17 +1,22 @@
import type { IRaceRegistrationRepository } from '@gridpilot/racing/domain/repositories/IRaceRegistrationRepository';
import type { GetRaceRegistrationsQueryParamsDTO } from '../dto/RaceRegistrationQueryDTO';
import type { IRaceRegistrationsPresenter } from '../presenters/IRaceRegistrationsPresenter';
/**
* Query object returning registered driver IDs for a race.
* Mirrors legacy getRegisteredDrivers behavior.
* Use Case: GetRaceRegistrationsUseCase
*
* Returns registered driver IDs for a race.
* Orchestrates domain logic and delegates presentation to the presenter.
*/
export class GetRaceRegistrationsQuery {
export class GetRaceRegistrationsUseCase {
constructor(
private readonly registrationRepository: IRaceRegistrationRepository,
public readonly presenter: IRaceRegistrationsPresenter,
) {}
async execute(params: GetRaceRegistrationsQueryParamsDTO): Promise<string[]> {
async execute(params: GetRaceRegistrationsQueryParamsDTO): Promise<void> {
const { raceId } = params;
return this.registrationRepository.getRegisteredDrivers(raceId);
const registeredDriverIds = await this.registrationRepository.getRegisteredDrivers(raceId);
this.presenter.present(registeredDriverIds);
}
}

View File

@@ -1,8 +1,9 @@
/**
* Application Query: GetRaceWithSOFQuery
*
* Use Case: GetRaceWithSOFUseCase
*
* Returns race details enriched with calculated Strength of Field (SOF).
* SOF is calculated from participant ratings if not already stored on the race.
* Orchestrates domain logic and delegates presentation to the presenter.
*/
import type { IRaceRepository } from '../../domain/repositories/IRaceRepository';
@@ -13,18 +14,13 @@ import {
AverageStrengthOfFieldCalculator,
type StrengthOfFieldCalculator,
} from '../../domain/services/StrengthOfFieldCalculator';
import type { RaceDTO } from '../dto/RaceDTO';
import type { IRaceWithSOFPresenter } from '../presenters/IRaceWithSOFPresenter';
export interface GetRaceWithSOFQueryParams {
raceId: string;
}
export interface RaceWithSOFDTO extends Omit<RaceDTO, 'strengthOfField'> {
strengthOfField: number | null;
participantCount: number;
}
export class GetRaceWithSOFQuery {
export class GetRaceWithSOFUseCase {
private readonly sofCalculator: StrengthOfFieldCalculator;
constructor(
@@ -32,12 +28,13 @@ export class GetRaceWithSOFQuery {
private readonly registrationRepository: IRaceRegistrationRepository,
private readonly resultRepository: IResultRepository,
private readonly driverRatingProvider: DriverRatingProvider,
public readonly presenter: IRaceWithSOFPresenter,
sofCalculator?: StrengthOfFieldCalculator,
) {
this.sofCalculator = sofCalculator ?? new AverageStrengthOfFieldCalculator();
}
async execute(params: GetRaceWithSOFQueryParams): Promise<RaceWithSOFDTO | null> {
async execute(params: GetRaceWithSOFQueryParams): Promise<void> {
const { raceId } = params;
const race = await this.raceRepository.findById(raceId);
@@ -69,20 +66,20 @@ export class GetRaceWithSOFQuery {
strengthOfField = this.sofCalculator.calculate(driverRatings);
}
return {
id: race.id,
leagueId: race.leagueId,
scheduledAt: race.scheduledAt.toISOString(),
track: race.track,
trackId: race.trackId,
car: race.car,
carId: race.carId,
sessionType: race.sessionType,
status: race.status,
this.presenter.present(
race.id,
race.leagueId,
race.scheduledAt,
race.track,
race.trackId,
race.car,
race.carId,
race.sessionType,
race.status,
strengthOfField,
registeredCount: race.registeredCount ?? participantIds.length,
maxParticipants: race.maxParticipants,
participantCount: participantIds.length,
};
race.registeredCount ?? participantIds.length,
race.maxParticipants,
participantIds.length
);
}
}

View File

@@ -0,0 +1,38 @@
import type { IRaceRepository } from '../../domain/repositories/IRaceRepository';
import type { ILeagueRepository } from '../../domain/repositories/ILeagueRepository';
import type { IRacesPagePresenter } from '@gridpilot/racing/application/presenters/IRacesPagePresenter';
export class GetRacesPageDataUseCase {
constructor(
private readonly raceRepository: IRaceRepository,
private readonly leagueRepository: ILeagueRepository,
public readonly presenter: IRacesPagePresenter,
) {}
async execute(): Promise<void> {
const [allRaces, allLeagues] = await Promise.all([
this.raceRepository.findAll(),
this.leagueRepository.findAll(),
]);
const leagueMap = new Map(allLeagues.map(l => [l.id, l.name]));
const races = allRaces
.sort((a, b) => a.scheduledAt.getTime() - b.scheduledAt.getTime())
.map(race => ({
id: race.id,
track: race.track,
car: race.car,
scheduledAt: race.scheduledAt.toISOString(),
status: race.status,
leagueId: race.leagueId,
leagueName: leagueMap.get(race.leagueId) ?? 'Unknown League',
strengthOfField: race.strengthOfField,
isUpcoming: race.isUpcoming(),
isLive: race.isLive(),
isPast: race.isPast(),
}));
this.presenter.present(races);
}
}

View File

@@ -1,6 +1,6 @@
/**
* Application Query: GetSponsorDashboardQuery
*
* Application Use Case: GetSponsorDashboardUseCase
*
* Returns sponsor dashboard metrics including sponsorships, impressions, and investment data.
*/
@@ -10,6 +10,7 @@ import type { ISeasonRepository } from '../../domain/repositories/ISeasonReposit
import type { ILeagueRepository } from '../../domain/repositories/ILeagueRepository';
import type { ILeagueMembershipRepository } from '../../domain/repositories/ILeagueMembershipRepository';
import type { IRaceRepository } from '../../domain/repositories/IRaceRepository';
import type { ISponsorDashboardPresenter } from '../presenters/ISponsorDashboardPresenter';
export interface GetSponsorDashboardQueryParams {
sponsorId: string;
@@ -46,7 +47,7 @@ export interface SponsorDashboardDTO {
};
}
export class GetSponsorDashboardQuery {
export class GetSponsorDashboardUseCase {
constructor(
private readonly sponsorRepository: ISponsorRepository,
private readonly seasonSponsorshipRepository: ISeasonSponsorshipRepository,
@@ -54,14 +55,16 @@ export class GetSponsorDashboardQuery {
private readonly leagueRepository: ILeagueRepository,
private readonly leagueMembershipRepository: ILeagueMembershipRepository,
private readonly raceRepository: IRaceRepository,
private readonly presenter: ISponsorDashboardPresenter,
) {}
async execute(params: GetSponsorDashboardQueryParams): Promise<SponsorDashboardDTO | null> {
async execute(params: GetSponsorDashboardQueryParams): Promise<void> {
const { sponsorId } = params;
const sponsor = await this.sponsorRepository.findById(sponsorId);
if (!sponsor) {
return null;
this.presenter.present(null);
return;
}
// Get all sponsorships for this sponsor
@@ -140,7 +143,7 @@ export class GetSponsorDashboardQuery {
? Math.min(100, (mainSponsorships * 30) + (sponsorships.length * 10))
: 0;
return {
this.presenter.present({
sponsorId,
sponsorName: sponsor.name,
metrics: {
@@ -159,6 +162,6 @@ export class GetSponsorDashboardQuery {
totalInvestment,
costPerThousandViews: Math.round(costPerThousandViews * 100) / 100,
},
};
});
}
}

View File

@@ -1,6 +1,6 @@
/**
* Application Query: GetSponsorSponsorshipsQuery
*
* Application Use Case: GetSponsorSponsorshipsUseCase
*
* Returns detailed sponsorship information for a sponsor's campaigns/sponsorships page.
*/
@@ -11,6 +11,7 @@ import type { ILeagueRepository } from '../../domain/repositories/ILeagueReposit
import type { ILeagueMembershipRepository } from '../../domain/repositories/ILeagueMembershipRepository';
import type { IRaceRepository } from '../../domain/repositories/IRaceRepository';
import type { SponsorshipTier, SponsorshipStatus } from '../../domain/entities/SeasonSponsorship';
import type { ISponsorSponsorshipsPresenter } from '../presenters/ISponsorSponsorshipsPresenter';
export interface GetSponsorSponsorshipsQueryParams {
sponsorId: string;
@@ -22,6 +23,8 @@ export interface SponsorshipDetailDTO {
leagueName: string;
seasonId: string;
seasonName: string;
seasonStartDate?: Date;
seasonEndDate?: Date;
tier: SponsorshipTier;
status: SponsorshipStatus;
pricing: {
@@ -59,7 +62,7 @@ export interface SponsorSponsorshipsDTO {
};
}
export class GetSponsorSponsorshipsQuery {
export class GetSponsorSponsorshipsUseCase {
constructor(
private readonly sponsorRepository: ISponsorRepository,
private readonly seasonSponsorshipRepository: ISeasonSponsorshipRepository,
@@ -67,14 +70,16 @@ export class GetSponsorSponsorshipsQuery {
private readonly leagueRepository: ILeagueRepository,
private readonly leagueMembershipRepository: ILeagueMembershipRepository,
private readonly raceRepository: IRaceRepository,
private readonly presenter: ISponsorSponsorshipsPresenter,
) {}
async execute(params: GetSponsorSponsorshipsQueryParams): Promise<SponsorSponsorshipsDTO | null> {
async execute(params: GetSponsorSponsorshipsQueryParams): Promise<void> {
const { sponsorId } = params;
const sponsor = await this.sponsorRepository.findById(sponsorId);
if (!sponsor) {
return null;
this.presenter.present(null);
return;
}
// Get all sponsorships for this sponsor
@@ -116,6 +121,8 @@ export class GetSponsorSponsorshipsQuery {
leagueName: league.name,
seasonId: season.id,
seasonName: season.name,
seasonStartDate: season.startDate,
seasonEndDate: season.endDate,
tier: sponsorship.tier,
status: sponsorship.status,
pricing: {
@@ -143,7 +150,7 @@ export class GetSponsorSponsorshipsQuery {
const activeSponsorships = sponsorships.filter(s => s.status === 'active').length;
return {
this.presenter.present({
sponsorId,
sponsorName: sponsor.name,
sponsorships: sponsorshipDetails,
@@ -154,6 +161,6 @@ export class GetSponsorSponsorshipsQuery {
totalPlatformFees,
currency: 'USD',
},
};
});
}
}

Some files were not shown because too many files have changed in this diff Show More