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

View File

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

View File

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

View File

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

View File

@@ -27,27 +27,15 @@ import Button from '@/components/ui/Button';
import Input from '@/components/ui/Input'; import Input from '@/components/ui/Input';
import Card from '@/components/ui/Card'; import Card from '@/components/ui/Card';
import Heading from '@/components/ui/Heading'; 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'; import Image from 'next/image';
// ============================================================================ // ============================================================================
// TYPES // TYPES
// ============================================================================ // ============================================================================
type SkillLevel = 'beginner' | 'intermediate' | 'advanced' | 'pro'; type DriverListItem = DriverLeaderboardItemViewModel;
interface DriverListItem {
id: string;
name: string;
rating: number;
skillLevel: SkillLevel;
nationality: string;
racesCompleted: number;
wins: number;
podiums: number;
isActive: boolean;
rank: number;
}
// ============================================================================ // ============================================================================
// DEMO DATA // DEMO DATA
@@ -87,7 +75,6 @@ interface FeaturedDriverCardProps {
} }
function FeaturedDriverCard({ driver, position, onClick }: FeaturedDriverCardProps) { function FeaturedDriverCard({ driver, position, onClick }: FeaturedDriverCardProps) {
const imageService = getImageService();
const levelConfig = SKILL_LEVELS.find((l) => l.id === driver.skillLevel); const levelConfig = SKILL_LEVELS.find((l) => l.id === driver.skillLevel);
const getBorderColor = (pos: number) => { const getBorderColor = (pos: number) => {
@@ -131,7 +118,7 @@ function FeaturedDriverCard({ driver, position, onClick }: FeaturedDriverCardPro
{/* Avatar & Name */} {/* Avatar & Name */}
<div className="flex items-center gap-4 mb-4"> <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"> <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>
<div> <div>
<h3 className="text-lg font-semibold text-white group-hover:text-primary-blue transition-colors"> <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) { function LeaderboardPreview({ drivers, onDriverClick }: LeaderboardPreviewProps) {
const router = useRouter(); const router = useRouter();
const imageService = getImageService();
const top5 = drivers.slice(0, 5); const top5 = drivers.slice(0, 5);
const getMedalColor = (position: number) => { const getMedalColor = (position: number) => {
@@ -300,7 +286,7 @@ function LeaderboardPreview({ drivers, onDriverClick }: LeaderboardPreviewProps)
{/* Avatar */} {/* Avatar */}
<div className="relative w-9 h-9 rounded-full overflow-hidden border-2 border-charcoal-outline"> <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> </div>
{/* Info */} {/* Info */}
@@ -345,7 +331,6 @@ interface RecentActivityProps {
} }
function RecentActivity({ drivers, onDriverClick }: RecentActivityProps) { function RecentActivity({ drivers, onDriverClick }: RecentActivityProps) {
const imageService = getImageService();
const activeDrivers = drivers.filter((d) => d.isActive).slice(0, 6); const activeDrivers = drivers.filter((d) => d.isActive).slice(0, 6);
return ( 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" 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"> <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 className="absolute bottom-0 right-0 w-3 h-3 rounded-full bg-performance-green border-2 border-iron-gray" />
</div> </div>
<p className="text-sm font-medium text-white truncate group-hover:text-performance-green transition-colors"> <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 [drivers, setDrivers] = useState<DriverListItem[]>([]);
const [loading, setLoading] = useState(true); const [loading, setLoading] = useState(true);
const [searchQuery, setSearchQuery] = useState(''); const [searchQuery, setSearchQuery] = useState('');
const [totalRaces, setTotalRaces] = useState(0);
const [totalWins, setTotalWins] = useState(0);
const [activeCount, setActiveCount] = useState(0);
useEffect(() => { useEffect(() => {
const load = async () => { const load = async () => {
const driverRepo = getDriverRepository(); const useCase = getGetDriversLeaderboardUseCase();
const allDrivers = await driverRepo.findAll(); await useCase.execute();
const rankings = getAllDriverRankings(); const viewModel = useCase.presenter.getViewModel();
const items: DriverListItem[] = allDrivers.map((driver) => { setDrivers(viewModel.drivers);
const stats = getDriverStats(driver.id); setTotalRaces(viewModel.totalRaces);
const rating = stats?.rating ?? 0; setTotalWins(viewModel.totalWins);
const wins = stats?.wins ?? 0; setActiveCount(viewModel.activeCount);
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);
setLoading(false); 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) // Featured drivers (top 4)
const featuredDrivers = filteredDrivers.slice(0, 4); const featuredDrivers = filteredDrivers.slice(0, 4);

View File

@@ -19,27 +19,17 @@ import {
import Button from '@/components/ui/Button'; import Button from '@/components/ui/Button';
import Input from '@/components/ui/Input'; import Input from '@/components/ui/Input';
import Heading from '@/components/ui/Heading'; 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'; import Image from 'next/image';
// ============================================================================ // ============================================================================
// TYPES // TYPES
// ============================================================================ // ============================================================================
type SkillLevel = 'beginner' | 'intermediate' | 'advanced' | 'pro';
type SortBy = 'rank' | 'rating' | 'wins' | 'podiums' | 'winRate'; type SortBy = 'rank' | 'rating' | 'wins' | 'podiums' | 'winRate';
interface DriverListItem { type DriverListItem = DriverLeaderboardItemViewModel;
id: string;
name: string;
rating: number;
skillLevel: SkillLevel;
nationality: string;
racesCompleted: number;
wins: number;
podiums: number;
rank: number;
}
// ============================================================================ // ============================================================================
// SKILL LEVEL CONFIG // SKILL LEVEL CONFIG
@@ -81,7 +71,6 @@ interface TopThreePodiumProps {
} }
function TopThreePodium({ drivers, onDriverClick }: TopThreePodiumProps) { function TopThreePodium({ drivers, onDriverClick }: TopThreePodiumProps) {
const imageService = getImageService();
const top3 = drivers.slice(0, 3); const top3 = drivers.slice(0, 3);
if (top3.length < 3) return null; if (top3.length < 3) return null;
@@ -122,7 +111,7 @@ function TopThreePodium({ drivers, onDriverClick }: TopThreePodiumProps) {
{/* Avatar */} {/* 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`}> <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 <Image
src={imageService.getDriverAvatar(driver.id)} src={driver.avatarUrl}
alt={driver.name} alt={driver.name}
fill fill
className="object-cover" className="object-cover"
@@ -178,7 +167,6 @@ function TopThreePodium({ drivers, onDriverClick }: TopThreePodiumProps) {
export default function DriverLeaderboardPage() { export default function DriverLeaderboardPage() {
const router = useRouter(); const router = useRouter();
const imageService = getImageService();
const [drivers, setDrivers] = useState<DriverListItem[]>([]); const [drivers, setDrivers] = useState<DriverListItem[]>([]);
const [loading, setLoading] = useState(true); const [loading, setLoading] = useState(true);
const [searchQuery, setSearchQuery] = useState(''); const [searchQuery, setSearchQuery] = useState('');
@@ -188,44 +176,10 @@ export default function DriverLeaderboardPage() {
useEffect(() => { useEffect(() => {
const load = async () => { const load = async () => {
const driverRepo = getDriverRepository(); const useCase = getGetDriversLeaderboardUseCase();
const allDrivers = await driverRepo.findAll(); await useCase.execute();
const rankings = getAllDriverRankings(); const viewModel = useCase.presenter.getViewModel();
setDrivers(viewModel.drivers);
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);
setLoading(false); setLoading(false);
}; };
@@ -443,7 +397,7 @@ export default function DriverLeaderboardPage() {
{/* Driver Info */} {/* Driver Info */}
<div className="col-span-5 lg:col-span-4 flex items-center gap-3"> <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"> <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>
<div className="min-w-0"> <div className="min-w-0">
<p className="text-white font-semibold truncate group-hover:text-primary-blue transition-colors"> <p className="text-white font-semibold truncate group-hover:text-primary-blue transition-colors">

View File

@@ -20,36 +20,18 @@ import {
} from 'lucide-react'; } from 'lucide-react';
import Button from '@/components/ui/Button'; import Button from '@/components/ui/Button';
import Heading from '@/components/ui/Heading'; 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 Image from 'next/image';
import type { Team } from '@gridpilot/racing';
// ============================================================================ // ============================================================================
// TYPES // TYPES
// ============================================================================ // ============================================================================
type SkillLevel = 'beginner' | 'intermediate' | 'advanced' | 'pro'; type DriverListItem = DriverLeaderboardItemViewModel;
interface DriverListItem { type TeamDisplayData = TeamLeaderboardItemViewModel;
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;
}
// ============================================================================ // ============================================================================
// SKILL LEVEL CONFIG // SKILL LEVEL CONFIG
@@ -80,7 +62,6 @@ interface DriverLeaderboardPreviewProps {
function DriverLeaderboardPreview({ drivers, onDriverClick }: DriverLeaderboardPreviewProps) { function DriverLeaderboardPreview({ drivers, onDriverClick }: DriverLeaderboardPreviewProps) {
const router = useRouter(); const router = useRouter();
const imageService = getImageService();
const top10 = drivers.slice(0, 10); const top10 = drivers.slice(0, 10);
const getMedalColor = (position: number) => { const getMedalColor = (position: number) => {
@@ -144,7 +125,7 @@ function DriverLeaderboardPreview({ drivers, onDriverClick }: DriverLeaderboardP
{/* Avatar */} {/* Avatar */}
<div className="relative w-9 h-9 rounded-full overflow-hidden border-2 border-charcoal-outline"> <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> </div>
{/* Info */} {/* Info */}
@@ -189,7 +170,6 @@ interface TeamLeaderboardPreviewProps {
function TeamLeaderboardPreview({ teams, onTeamClick }: TeamLeaderboardPreviewProps) { function TeamLeaderboardPreview({ teams, onTeamClick }: TeamLeaderboardPreviewProps) {
const router = useRouter(); const router = useRouter();
const imageService = getImageService();
const top5 = [...teams] const top5 = [...teams]
.filter((t) => t.rating !== null) .filter((t) => t.rating !== null)
.sort((a, b) => (b.rating ?? 0) - (a.rating ?? 0)) .sort((a, b) => (b.rating ?? 0) - (a.rating ?? 0))
@@ -304,99 +284,16 @@ export default function LeaderboardsPage() {
useEffect(() => { useEffect(() => {
const load = async () => { const load = async () => {
try { try {
// Load drivers const driversUseCase = getGetDriversLeaderboardUseCase();
const driverRepo = getDriverRepository(); const teamsUseCase = getGetTeamsLeaderboardUseCase();
const allDrivers = await driverRepo.findAll(); await driversUseCase.execute();
const rankings = getAllDriverRankings(); await teamsUseCase.execute();
const driverItems: DriverListItem[] = allDrivers.map((driver) => { const driversViewModel = driversUseCase.presenter.getViewModel();
const stats = getDriverStats(driver.id); const teamsViewModel = teamsUseCase.presenter.getViewModel();
const rating = stats?.rating ?? 0;
const wins = stats?.wins ?? 0;
const podiums = stats?.podiums ?? 0;
let effectiveRank = Number.POSITIVE_INFINITY; setDrivers(driversViewModel.drivers);
if (typeof stats?.overallRank === 'number' && stats.overallRank > 0) { setTeams(teamsViewModel.teams);
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);
} catch (error) { } catch (error) {
console.error('Failed to load leaderboard data:', error); console.error('Failed to load leaderboard data:', error);
setDrivers([]); setDrivers([]);

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -32,57 +32,73 @@ interface Sponsorship {
drivers: number; drivers: number;
} }
// Mock data - in production would come from repository interface SponsorshipDetailApi {
const MOCK_SPONSORSHIPS: Sponsorship[] = [ id: string;
{ leagueId: string;
id: 'sp-1', leagueName: string;
leagueId: 'league-1', seasonId: string;
leagueName: 'GT3 Pro Championship', seasonName: string;
tier: 'main', seasonStartDate?: string;
status: 'active', seasonEndDate?: string;
startDate: new Date('2025-01-01'), tier: 'main' | 'secondary';
endDate: new Date('2025-06-30'), status: string;
price: 1200, pricing: {
impressions: 45200, amount: number;
drivers: 32, currency: string;
}, };
{ metrics: {
id: 'sp-2', drivers: number;
leagueId: 'league-2', races: number;
leagueName: 'Endurance Masters', completedRaces: number;
tier: 'main', impressions: number;
status: 'active', };
startDate: new Date('2025-02-01'), createdAt: string;
endDate: new Date('2025-07-31'), activatedAt?: string;
price: 1000, }
impressions: 38100,
drivers: 48, interface SponsorSponsorshipsResponse {
}, sponsorId: string;
{ sponsorName: string;
id: 'sp-3', sponsorships: SponsorshipDetailApi[];
leagueId: 'league-3', summary: {
leagueName: 'Formula Sim Series', totalSponsorships: number;
tier: 'secondary', activeSponsorships: number;
status: 'active', totalInvestment: number;
startDate: new Date('2025-03-01'), totalPlatformFees: number;
endDate: new Date('2025-08-31'), currency: string;
price: 400, };
impressions: 22800, }
drivers: 24,
}, function mapSponsorshipStatus(status: string): 'active' | 'pending' | 'expired' {
{ switch (status) {
id: 'sp-4', case 'active':
leagueId: 'league-4', return 'active';
leagueName: 'Touring Car Cup', case 'pending':
tier: 'secondary', return 'pending';
status: 'pending', default:
startDate: new Date('2025-04-01'), return 'expired';
endDate: new Date('2025-09-30'), }
price: 350, }
impressions: 0,
drivers: 28, 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 }) { function SponsorshipCard({ sponsorship }: { sponsorship: Sponsorship }) {
const router = useRouter(); const router = useRouter();
@@ -179,18 +195,59 @@ function SponsorshipCard({ sponsorship }: { sponsorship: Sponsorship }) {
export default function SponsorCampaignsPage() { export default function SponsorCampaignsPage() {
const router = useRouter(); const router = useRouter();
const [filter, setFilter] = useState<'all' | 'active' | 'pending' | 'expired'>('all'); const [filter, setFilter] = useState<'all' | 'active' | 'pending' | 'expired'>('all');
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' const filteredSponsorships = filter === 'all'
? MOCK_SPONSORSHIPS ? sponsorships
: MOCK_SPONSORSHIPS.filter(s => s.status === filter); : sponsorships.filter(s => s.status === filter);
const stats = { const stats = {
total: MOCK_SPONSORSHIPS.length, total: sponsorships.length,
active: MOCK_SPONSORSHIPS.filter(s => s.status === 'active').length, active: sponsorships.filter(s => s.status === 'active').length,
pending: MOCK_SPONSORSHIPS.filter(s => s.status === 'pending').length, pending: sponsorships.filter(s => s.status === 'pending').length,
totalInvestment: MOCK_SPONSORSHIPS.reduce((sum, s) => sum + s.price, 0), 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 ( return (
<div className="max-w-6xl mx-auto py-8 px-4"> <div className="max-w-6xl mx-auto py-8 px-4">
{/* Header */} {/* 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({ function MetricCard({
title, title,
@@ -205,15 +147,13 @@ export default function SponsorDashboardPage() {
try { try {
const response = await fetch('/api/sponsors/dashboard'); const response = await fetch('/api/sponsors/dashboard');
if (response.ok) { if (response.ok) {
const dashboardData = await response.json(); const dashboardData: SponsorDashboardData = await response.json();
setData(dashboardData); setData(dashboardData);
} else { } else {
// Use mock data for demo mode setError('Failed to load sponsor dashboard');
setData(MOCK_DASHBOARD);
} }
} catch { } catch {
// Use mock data on error setError('Failed to load sponsor dashboard');
setData(MOCK_DASHBOARD);
} finally { } finally {
setLoading(false); 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 ( return (
<div className="max-w-7xl mx-auto py-8 px-4"> <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 TeamAdmin from '@/components/teams/TeamAdmin';
import JoinTeamButton from '@/components/teams/JoinTeamButton'; import JoinTeamButton from '@/components/teams/JoinTeamButton';
import { import {
getGetTeamDetailsQuery, getGetTeamDetailsUseCase,
getGetTeamMembersQuery, getGetTeamMembersUseCase,
getTeamMembershipRepository, getTeamMembershipRepository,
} from '@/lib/di-container'; } from '@/lib/di-container';
import { useEffectiveDriverId } from '@/lib/currentDriver'; import { useEffectiveDriverId } from '@/lib/currentDriver';
@@ -39,11 +39,15 @@ export default function TeamDetailPage() {
const loadTeamData = useCallback(async () => { const loadTeamData = useCallback(async () => {
setLoading(true); setLoading(true);
try { try {
const detailsQuery = getGetTeamDetailsQuery(); const detailsUseCase = getGetTeamDetailsUseCase();
const membersQuery = getGetTeamMembersQuery(); const membersUseCase = getGetTeamMembersUseCase();
const details = await detailsQuery.execute({ teamId, driverId: currentDriverId }); await detailsUseCase.execute({ teamId, driverId: currentDriverId });
const teamMemberships = await membersQuery.execute({ teamId }); const detailsViewModel = detailsUseCase.presenter.getViewModel();
await membersUseCase.execute({ teamId });
const membersViewModel = membersUseCase.presenter.getViewModel();
const teamMemberships = membersViewModel.members;
const adminStatus = const adminStatus =
teamMemberships.some( teamMemberships.some(
@@ -52,7 +56,7 @@ export default function TeamDetailPage() {
(m.role === 'owner' || m.role === 'manager'), (m.role === 'owner' || m.role === 'manager'),
) ?? false; ) ?? false;
setTeam(details.team); setTeam(detailsViewModel.team);
setMemberships(teamMemberships); setMemberships(teamMemberships);
setIsAdmin(adminStatus); setIsAdmin(adminStatus);
} finally { } finally {

View File

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

View File

@@ -28,30 +28,14 @@ import Card from '@/components/ui/Card';
import Input from '@/components/ui/Input'; import Input from '@/components/ui/Input';
import Heading from '@/components/ui/Heading'; import Heading from '@/components/ui/Heading';
import CreateTeamForm from '@/components/teams/CreateTeamForm'; import CreateTeamForm from '@/components/teams/CreateTeamForm';
import { getGetAllTeamsQuery, getGetTeamMembersQuery, getDriverStats } from '@/lib/di-container'; import { getGetTeamsLeaderboardUseCase } from '@/lib/di-container';
import type { Team } from '@gridpilot/racing'; import type { TeamLeaderboardItemViewModel, SkillLevel } from '@gridpilot/racing/application/presenters/ITeamsLeaderboardPresenter';
// ============================================================================ // ============================================================================
// TYPES // TYPES
// ============================================================================ // ============================================================================
type SkillLevel = 'beginner' | 'intermediate' | 'advanced' | 'pro'; type TeamDisplayData = TeamLeaderboardItemViewModel;
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[];
}
// ============================================================================ // ============================================================================
// SKILL LEVEL CONFIG // SKILL LEVEL CONFIG
@@ -463,59 +447,10 @@ export default function TeamsPage() {
const loadTeams = async () => { const loadTeams = async () => {
try { try {
const allTeamsQuery = getGetAllTeamsQuery(); const useCase = getGetTeamsLeaderboardUseCase();
const teamMembersQuery = getGetTeamMembersQuery(); await useCase.execute();
const viewModel = useCase.presenter.getViewModel();
const allTeams = await allTeamsQuery.execute(); setRealTeams(viewModel.teams);
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);
} catch (error) { } catch (error) {
console.error('Failed to load teams:', error); console.error('Failed to load teams:', error);
} finally { } finally {

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -64,38 +64,41 @@ export const DI_TOKENS = {
MarkNotificationReadUseCase: Symbol.for('MarkNotificationReadUseCase'), MarkNotificationReadUseCase: Symbol.for('MarkNotificationReadUseCase'),
// Queries - Racing // Queries - Racing
IsDriverRegisteredForRaceQuery: Symbol.for('IsDriverRegisteredForRaceQuery'), IsDriverRegisteredForRaceUseCase: Symbol.for('IsDriverRegisteredForRaceUseCase'),
GetRaceRegistrationsQuery: Symbol.for('GetRaceRegistrationsQuery'), GetRaceRegistrationsUseCase: Symbol.for('GetRaceRegistrationsUseCase'),
GetLeagueStandingsQuery: Symbol.for('GetLeagueStandingsQuery'), GetLeagueStandingsUseCase: Symbol.for('GetLeagueStandingsUseCase'),
GetLeagueDriverSeasonStatsQuery: Symbol.for('GetLeagueDriverSeasonStatsQuery'), GetLeagueDriverSeasonStatsUseCase: Symbol.for('GetLeagueDriverSeasonStatsUseCase'),
GetAllLeaguesWithCapacityQuery: Symbol.for('GetAllLeaguesWithCapacityQuery'), GetAllLeaguesWithCapacityUseCase: Symbol.for('GetAllLeaguesWithCapacityUseCase'),
GetAllLeaguesWithCapacityAndScoringQuery: Symbol.for('GetAllLeaguesWithCapacityAndScoringQuery'), GetAllLeaguesWithCapacityAndScoringUseCase: Symbol.for('GetAllLeaguesWithCapacityAndScoringUseCase'),
ListLeagueScoringPresetsQuery: Symbol.for('ListLeagueScoringPresetsQuery'), ListLeagueScoringPresetsUseCase: Symbol.for('ListLeagueScoringPresetsUseCase'),
GetLeagueScoringConfigQuery: Symbol.for('GetLeagueScoringConfigQuery'), GetLeagueScoringConfigUseCase: Symbol.for('GetLeagueScoringConfigUseCase'),
GetLeagueFullConfigQuery: Symbol.for('GetLeagueFullConfigQuery'), GetLeagueFullConfigUseCase: Symbol.for('GetLeagueFullConfigUseCase'),
PreviewLeagueScheduleQuery: Symbol.for('PreviewLeagueScheduleQuery'), PreviewLeagueScheduleUseCase: Symbol.for('PreviewLeagueScheduleUseCase'),
GetRaceWithSOFQuery: Symbol.for('GetRaceWithSOFQuery'), GetRaceWithSOFUseCase: Symbol.for('GetRaceWithSOFUseCase'),
GetLeagueStatsQuery: Symbol.for('GetLeagueStatsQuery'), GetLeagueStatsUseCase: Symbol.for('GetLeagueStatsUseCase'),
GetRacesPageDataUseCase: Symbol.for('GetRacesPageDataUseCase'),
GetDriversLeaderboardUseCase: Symbol.for('GetDriversLeaderboardUseCase'),
GetTeamsLeaderboardUseCase: Symbol.for('GetTeamsLeaderboardUseCase'),
// Queries - Teams // Use Cases - Teams (Query-like)
GetAllTeamsQuery: Symbol.for('GetAllTeamsQuery'), GetAllTeamsUseCase: Symbol.for('GetAllTeamsUseCase'),
GetTeamDetailsQuery: Symbol.for('GetTeamDetailsQuery'), GetTeamDetailsUseCase: Symbol.for('GetTeamDetailsUseCase'),
GetTeamMembersQuery: Symbol.for('GetTeamMembersQuery'), GetTeamMembersUseCase: Symbol.for('GetTeamMembersUseCase'),
GetTeamJoinRequestsQuery: Symbol.for('GetTeamJoinRequestsQuery'), GetTeamJoinRequestsUseCase: Symbol.for('GetTeamJoinRequestsUseCase'),
GetDriverTeamQuery: Symbol.for('GetDriverTeamQuery'), GetDriverTeamUseCase: Symbol.for('GetDriverTeamUseCase'),
// Queries - Stewarding // Queries - Stewarding
GetRaceProtestsQuery: Symbol.for('GetRaceProtestsQuery'), GetRaceProtestsUseCase: Symbol.for('GetRaceProtestsUseCase'),
GetRacePenaltiesQuery: Symbol.for('GetRacePenaltiesQuery'), GetRacePenaltiesUseCase: Symbol.for('GetRacePenaltiesUseCase'),
// Queries - Notifications // Queries - Notifications
GetUnreadNotificationsQuery: Symbol.for('GetUnreadNotificationsQuery'), GetUnreadNotificationsQuery: Symbol.for('GetUnreadNotificationsQuery'),
// Queries - Sponsors // Use Cases - Sponsors
GetSponsorDashboardQuery: Symbol.for('GetSponsorDashboardQuery'), GetSponsorDashboardUseCase: Symbol.for('GetSponsorDashboardUseCase'),
GetSponsorSponsorshipsQuery: Symbol.for('GetSponsorSponsorshipsQuery'), GetSponsorSponsorshipsUseCase: Symbol.for('GetSponsorSponsorshipsUseCase'),
GetPendingSponsorshipRequestsQuery: Symbol.for('GetPendingSponsorshipRequestsQuery'), GetPendingSponsorshipRequestsUseCase: Symbol.for('GetPendingSponsorshipRequestsUseCase'),
GetEntitySponsorshipPricingQuery: Symbol.for('GetEntitySponsorshipPricingQuery'), GetEntitySponsorshipPricingUseCase: Symbol.for('GetEntitySponsorshipPricingUseCase'),
// Use Cases - Sponsorship // Use Cases - Sponsorship
ApplyForSponsorshipUseCase: Symbol.for('ApplyForSponsorshipUseCase'), ApplyForSponsorshipUseCase: Symbol.for('ApplyForSponsorshipUseCase'),
@@ -104,6 +107,20 @@ export const DI_TOKENS = {
// Data // Data
DriverStats: Symbol.for('DriverStats'), 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; } as const;
export type DITokens = typeof DI_TOKENS; 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 { ISeasonRepository } from '../../domain/repositories/ISeasonRepository';
import type { ILeagueScoringConfigRepository } from '../../domain/repositories/ILeagueScoringConfigRepository'; import type { ILeagueScoringConfigRepository } from '../../domain/repositories/ILeagueScoringConfigRepository';
import type { IGameRepository } from '../../domain/repositories/IGameRepository'; import type { IGameRepository } from '../../domain/repositories/IGameRepository';
import type { import type { LeagueScoringPresetProvider } from '../ports/LeagueScoringPresetProvider';
LeagueScoringPresetProvider, import type { IAllLeaguesWithCapacityAndScoringPresenter, LeagueEnrichedData } from '../presenters/IAllLeaguesWithCapacityAndScoringPresenter';
LeagueScoringPresetDTO,
} from '../ports/LeagueScoringPresetProvider';
import type {
LeagueSummaryDTO,
LeagueSummaryScoringDTO,
} from '../dto/LeagueSummaryDTO';
/** /**
* Combined capacity + scoring summary query for leagues. * Use Case for retrieving all leagues with capacity and scoring information.
* * Orchestrates domain logic and delegates presentation to the presenter.
* Extends the behavior of GetAllLeaguesWithCapacityQuery by including
* scoring preset and game summaries when an active season and
* LeagueScoringConfig are available.
*/ */
export class GetAllLeaguesWithCapacityAndScoringQuery { export class GetAllLeaguesWithCapacityAndScoringUseCase {
constructor( constructor(
private readonly leagueRepository: ILeagueRepository, private readonly leagueRepository: ILeagueRepository,
private readonly leagueMembershipRepository: ILeagueMembershipRepository, private readonly leagueMembershipRepository: ILeagueMembershipRepository,
@@ -27,17 +18,16 @@ export class GetAllLeaguesWithCapacityAndScoringQuery {
private readonly leagueScoringConfigRepository: ILeagueScoringConfigRepository, private readonly leagueScoringConfigRepository: ILeagueScoringConfigRepository,
private readonly gameRepository: IGameRepository, private readonly gameRepository: IGameRepository,
private readonly presetProvider: LeagueScoringPresetProvider, private readonly presetProvider: LeagueScoringPresetProvider,
public readonly presenter: IAllLeaguesWithCapacityAndScoringPresenter,
) {} ) {}
async execute(): Promise<LeagueSummaryDTO[]> { async execute(): Promise<void> {
const leagues = await this.leagueRepository.findAll(); const leagues = await this.leagueRepository.findAll();
const results: LeagueSummaryDTO[] = []; const enrichedLeagues: LeagueEnrichedData[] = [];
for (const league of leagues) { for (const league of leagues) {
const members = await this.leagueMembershipRepository.getLeagueMembers( const members = await this.leagueMembershipRepository.getLeagueMembers(league.id);
league.id,
);
const usedDriverSlots = members.filter( const usedDriverSlots = members.filter(
(m) => (m) =>
@@ -48,116 +38,36 @@ export class GetAllLeaguesWithCapacityAndScoringQuery {
m.role === 'member'), m.role === 'member'),
).length; ).length;
const configuredMaxDrivers = league.settings.maxDrivers ?? usedDriverSlots; const seasons = await this.seasonRepository.findByLeagueId(league.id);
const safeMaxDrivers = Math.max(configuredMaxDrivers, usedDriverSlots); 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; enrichedLeagues.push({
const mainRaceMinutes = league,
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,
usedDriverSlots, usedDriverSlots,
maxTeams: undefined, season: activeSeason,
usedTeamSlots: undefined, scoringConfig,
structureSummary, game,
scoringPatternSummary: scoringSummary?.scoringPatternSummary, preset,
timingSummary, });
scoring: scoringSummary,
};
results.push(dto);
} }
return results; this.presenter.present(enrichedLeagues);
}
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';
} }
} }

View File

@@ -1,17 +1,22 @@
import type { ILeagueRepository } from '../../domain/repositories/ILeagueRepository'; import type { ILeagueRepository } from '../../domain/repositories/ILeagueRepository';
import type { ILeagueMembershipRepository } from '../../domain/repositories/ILeagueMembershipRepository'; 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( constructor(
private readonly leagueRepository: ILeagueRepository, private readonly leagueRepository: ILeagueRepository,
private readonly leagueMembershipRepository: ILeagueMembershipRepository, private readonly leagueMembershipRepository: ILeagueMembershipRepository,
public readonly presenter: IAllLeaguesWithCapacityPresenter,
) {} ) {}
async execute(): Promise<LeagueDTO[]> { async execute(): Promise<void> {
const leagues = await this.leagueRepository.findAll(); const leagues = await this.leagueRepository.findAll();
const results: LeagueDTO[] = []; const memberCounts = new Map<string, number>();
for (const league of leagues) { for (const league of leagues) {
const members = await this.leagueMembershipRepository.getLeagueMembers(league.id); const members = await this.leagueMembershipRepository.getLeagueMembers(league.id);
@@ -25,34 +30,9 @@ export class GetAllLeaguesWithCapacityQuery {
m.role === 'member'), m.role === 'member'),
).length; ).length;
// Ensure we never expose an impossible state like 26/24: memberCounts.set(league.id, usedSlots);
// 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);
} }
return results; this.presenter.present(leagues, memberCounts);
} }
} }

View File

@@ -1,13 +1,32 @@
import type { ITeamRepository } from '../../domain/repositories/ITeamRepository'; 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( constructor(
private readonly teamRepository: ITeamRepository, 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(); 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 { ITeamRepository } from '../../domain/repositories/ITeamRepository';
import type { ITeamMembershipRepository } from '../../domain/repositories/ITeamMembershipRepository'; import type { ITeamMembershipRepository } from '../../domain/repositories/ITeamMembershipRepository';
import type { import type { IDriverTeamPresenter } from '../presenters/IDriverTeamPresenter';
GetDriverTeamQueryParamsDTO,
GetDriverTeamQueryResultDTO,
} from '../dto/TeamCommandAndQueryDTO';
export class GetDriverTeamQuery { /**
* Use Case for retrieving a driver's team.
* Orchestrates domain logic and delegates presentation to the presenter.
*/
export class GetDriverTeamUseCase {
constructor( constructor(
private readonly teamRepository: ITeamRepository, private readonly teamRepository: ITeamRepository,
private readonly membershipRepository: ITeamMembershipRepository, private readonly membershipRepository: ITeamMembershipRepository,
public readonly presenter: IDriverTeamPresenter,
) {} ) {}
async execute(params: GetDriverTeamQueryParamsDTO): Promise<GetDriverTeamQueryResultDTO | null> { async execute(driverId: string): Promise<boolean> {
const { driverId } = params;
const membership = await this.membershipRepository.getActiveMembershipForDriver(driverId); const membership = await this.membershipRepository.getActiveMembershipForDriver(driverId);
if (!membership) { if (!membership) {
return null; return false;
} }
const team = await this.teamRepository.findById(membership.teamId); const team = await this.teamRepository.findById(membership.teamId);
if (!team) { 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,5 +1,5 @@
/** /**
* Query: GetEntitySponsorshipPricingQuery * Application Use Case: GetEntitySponsorshipPricingUseCase
* *
* Retrieves sponsorship pricing configuration for any entity. * Retrieves sponsorship pricing configuration for any entity.
* Used by sponsors to see available slots and prices. * 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 { ISeasonSponsorshipRepository } from '../../domain/repositories/ISeasonSponsorshipRepository';
import type { SponsorableEntityType } from '../../domain/entities/SponsorshipRequest'; import type { SponsorableEntityType } from '../../domain/entities/SponsorshipRequest';
import type { SponsorshipTier } from '../../domain/entities/SeasonSponsorship'; import type { SponsorshipTier } from '../../domain/entities/SeasonSponsorship';
import type { IEntitySponsorshipPricingPresenter } from '../presenters/IEntitySponsorshipPricingPresenter';
export interface GetEntitySponsorshipPricingDTO { export interface GetEntitySponsorshipPricingDTO {
entityType: SponsorableEntityType; entityType: SponsorableEntityType;
@@ -37,18 +38,20 @@ export interface GetEntitySponsorshipPricingResultDTO {
secondarySlot?: SponsorshipSlotDTO; secondarySlot?: SponsorshipSlotDTO;
} }
export class GetEntitySponsorshipPricingQuery { export class GetEntitySponsorshipPricingUseCase {
constructor( constructor(
private readonly sponsorshipPricingRepo: ISponsorshipPricingRepository, private readonly sponsorshipPricingRepo: ISponsorshipPricingRepository,
private readonly sponsorshipRequestRepo: ISponsorshipRequestRepository, private readonly sponsorshipRequestRepo: ISponsorshipRequestRepository,
private readonly seasonSponsorshipRepo: ISeasonSponsorshipRepository, 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); const pricing = await this.sponsorshipPricingRepo.findByEntity(dto.entityType, dto.entityId);
if (!pricing) { if (!pricing) {
return null; this.presenter.present(null);
return;
} }
// Count pending requests by tier // 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 { IResultRepository } from '../../domain/repositories/IResultRepository';
import type { IPenaltyRepository } from '../../domain/repositories/IPenaltyRepository'; import type { IPenaltyRepository } from '../../domain/repositories/IPenaltyRepository';
import type { IRaceRepository } from '../../domain/repositories/IRaceRepository'; import type { IRaceRepository } from '../../domain/repositories/IRaceRepository';
import type { LeagueDriverSeasonStatsDTO } from '../dto/LeagueDriverSeasonStatsDTO'; import type { ILeagueDriverSeasonStatsPresenter } from '../presenters/ILeagueDriverSeasonStatsPresenter';
export interface DriverRatingPort { export interface DriverRatingPort {
getRating(driverId: string): { rating: number | null; ratingChange: number | null }; getRating(driverId: string): { rating: number | null; ratingChange: number | null };
} }
export interface GetLeagueDriverSeasonStatsQueryParamsDTO { export interface GetLeagueDriverSeasonStatsUseCaseParams {
leagueId: string; 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( constructor(
private readonly standingRepository: IStandingRepository, private readonly standingRepository: IStandingRepository,
private readonly resultRepository: IResultRepository, private readonly resultRepository: IResultRepository,
private readonly penaltyRepository: IPenaltyRepository, private readonly penaltyRepository: IPenaltyRepository,
private readonly raceRepository: IRaceRepository, private readonly raceRepository: IRaceRepository,
private readonly driverRatingPort: DriverRatingPort, private readonly driverRatingPort: DriverRatingPort,
public readonly presenter: ILeagueDriverSeasonStatsPresenter,
) {} ) {}
async execute(params: GetLeagueDriverSeasonStatsQueryParamsDTO): Promise<LeagueDriverSeasonStatsDTO[]> { async execute(params: GetLeagueDriverSeasonStatsUseCaseParams): Promise<void> {
const { leagueId } = params; const { leagueId } = params;
// Get standings and races for the league // Get standings and races for the league
@@ -53,59 +58,26 @@ export class GetLeagueDriverSeasonStatsQuery {
penaltiesByDriver.set(p.driverId, current); penaltiesByDriver.set(p.driverId, current);
} }
// Build basic stats per driver from standings // Collect driver ratings
const statsByDriver = new Map<string, LeagueDriverSeasonStatsDTO>(); const driverRatings = new Map<string, { rating: number | null; ratingChange: number | null }>();
for (const standing of standings) { 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 ratingInfo = this.driverRatingPort.getRating(standing.driverId);
driverRatings.set(standing.driverId, ratingInfo);
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);
} }
// Enhance stats with basic finish-position-based avgFinish from results // Collect driver results
for (const [driverId, dto] of statsByDriver.entries()) { const driverResults = new Map<string, Array<{ position: number }>>();
const driverResults = await this.resultRepository.findByDriverIdAndLeagueId(driverId, leagueId); for (const standing of standings) {
if (driverResults.length > 0) { const results = await this.resultRepository.findByDriverIdAndLeagueId(standing.driverId, leagueId);
const totalPositions = driverResults.reduce((sum, r) => sum + r.position, 0); driverResults.set(standing.driverId, results);
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);
} }
// Ensure ordering by position this.presenter.present(
const result = Array.from(statsByDriver.values()).sort((a, b) => a.position - b.position); leagueId,
standings,
return result; 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 { ISeasonRepository } from '../../domain/repositories/ISeasonRepository';
import type { ILeagueScoringConfigRepository } from '../../domain/repositories/ILeagueScoringConfigRepository'; import type { ILeagueScoringConfigRepository } from '../../domain/repositories/ILeagueScoringConfigRepository';
import type { IGameRepository } from '../../domain/repositories/IGameRepository'; import type { IGameRepository } from '../../domain/repositories/IGameRepository';
import type { ChampionshipConfig } from '../../domain/value-objects/ChampionshipConfig'; import type { ILeagueFullConfigPresenter, LeagueFullConfigData } from '../presenters/ILeagueFullConfigPresenter';
import type { DropScorePolicy } from '../../domain/value-objects/DropScorePolicy';
import type {
LeagueConfigFormModel,
LeagueDropPolicyFormDTO,
} from '../dto/LeagueConfigFormDTO';
/** /**
* Query returning a unified LeagueConfigFormModel for a given league. * Use Case for retrieving a league's full configuration.
* * Orchestrates domain logic and delegates presentation to the presenter.
* 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
*/ */
export class GetLeagueFullConfigQuery { export class GetLeagueFullConfigUseCase {
constructor( constructor(
private readonly leagueRepository: ILeagueRepository, private readonly leagueRepository: ILeagueRepository,
private readonly seasonRepository: ISeasonRepository, private readonly seasonRepository: ISeasonRepository,
private readonly leagueScoringConfigRepository: ILeagueScoringConfigRepository, private readonly leagueScoringConfigRepository: ILeagueScoringConfigRepository,
private readonly gameRepository: IGameRepository, 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 { leagueId } = params;
const league = await this.leagueRepository.findById(leagueId); const league = await this.leagueRepository.findById(leagueId);
if (!league) { if (!league) {
return null; throw new Error(`League ${leagueId} not found`);
} }
const seasons = await this.seasonRepository.findByLeagueId(leagueId); const seasons = await this.seasonRepository.findByLeagueId(leagueId);
const activeSeason = const activeSeason =
seasons && seasons.length > 0 seasons && seasons.length > 0
? seasons.find((s) => s.status === 'active') ?? seasons[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; : undefined;
const dropPolicy: DropScorePolicy | undefined = let scoringConfig;
primaryChampionship?.dropScorePolicy ?? undefined; let game;
const dropPolicyForm: LeagueDropPolicyFormDTO = this.mapDropPolicy(dropPolicy); if (activeSeason) {
scoringConfig = await this.leagueScoringConfigRepository.findBySeasonId(activeSeason.id);
const defaultQualifyingMinutes = 30; if (activeSeason.gameId) {
const defaultMainRaceMinutes = 40; game = await this.gameRepository.findById(activeSeason.gameId);
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 data: LeagueFullConfigData = {
const sprintRaceMinutes = patternId === 'sprint-main-driver' ? 20 : undefined; league,
activeSeason,
const form: LeagueConfigFormModel = { scoringConfig,
leagueId: league.id, game,
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,
},
}; };
return form; this.presenter.present(data);
}
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' };
} }
} }

View File

@@ -2,41 +2,34 @@ import type { ILeagueRepository } from '../../domain/repositories/ILeagueReposit
import type { ISeasonRepository } from '../../domain/repositories/ISeasonRepository'; import type { ISeasonRepository } from '../../domain/repositories/ISeasonRepository';
import type { ILeagueScoringConfigRepository } from '../../domain/repositories/ILeagueScoringConfigRepository'; import type { ILeagueScoringConfigRepository } from '../../domain/repositories/ILeagueScoringConfigRepository';
import type { IGameRepository } from '../../domain/repositories/IGameRepository'; import type { IGameRepository } from '../../domain/repositories/IGameRepository';
import type { LeagueScoringConfigDTO } from '../dto/LeagueScoringConfigDTO'; import type { LeagueScoringPresetProvider } from '../ports/LeagueScoringPresetProvider';
import type { LeagueScoringChampionshipDTO } from '../dto/LeagueScoringConfigDTO'; import type { ILeagueScoringConfigPresenter, LeagueScoringConfigData } from '../presenters/ILeagueScoringConfigPresenter';
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';
/** /**
* Query returning a league's scoring configuration for its active season. * Use Case for retrieving a league's scoring configuration for its active season.
* * Orchestrates domain logic and delegates presentation to the presenter.
* Designed for the league detail "Scoring" tab.
*/ */
export class GetLeagueScoringConfigQuery { export class GetLeagueScoringConfigUseCase {
constructor( constructor(
private readonly leagueRepository: ILeagueRepository, private readonly leagueRepository: ILeagueRepository,
private readonly seasonRepository: ISeasonRepository, private readonly seasonRepository: ISeasonRepository,
private readonly leagueScoringConfigRepository: ILeagueScoringConfigRepository, private readonly leagueScoringConfigRepository: ILeagueScoringConfigRepository,
private readonly gameRepository: IGameRepository, private readonly gameRepository: IGameRepository,
private readonly presetProvider: LeagueScoringPresetProvider, 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 { leagueId } = params;
const league = await this.leagueRepository.findById(leagueId); const league = await this.leagueRepository.findById(leagueId);
if (!league) { if (!league) {
return null; throw new Error(`League ${leagueId} not found`);
} }
const seasons = await this.seasonRepository.findByLeagueId(leagueId); const seasons = await this.seasonRepository.findByLeagueId(leagueId);
if (!seasons || seasons.length === 0) { if (!seasons || seasons.length === 0) {
return null; throw new Error(`No seasons found for league ${leagueId}`);
} }
const activeSeason = const activeSeason =
@@ -45,146 +38,27 @@ export class GetLeagueScoringConfigQuery {
const scoringConfig = const scoringConfig =
await this.leagueScoringConfigRepository.findBySeasonId(activeSeason.id); await this.leagueScoringConfigRepository.findBySeasonId(activeSeason.id);
if (!scoringConfig) { if (!scoringConfig) {
return null; throw new Error(`No scoring config found for season ${activeSeason.id}`);
} }
const game = await this.gameRepository.findById(activeSeason.gameId); const game = await this.gameRepository.findById(activeSeason.gameId);
if (!game) { if (!game) {
return null; throw new Error(`Game ${activeSeason.gameId} not found`);
} }
const presetId = scoringConfig.scoringPresetId; const presetId = scoringConfig.scoringPresetId;
const preset: LeagueScoringPresetDTO | undefined = const preset = presetId ? this.presetProvider.getPresetById(presetId) : undefined;
presetId ? this.presetProvider.getPresetById(presetId) : undefined;
const championships: LeagueScoringChampionshipDTO[] = const data: LeagueScoringConfigData = {
scoringConfig.championships.map((champ) =>
this.mapChampionship(champ),
);
const dropPolicySummary =
preset?.dropPolicySummary ??
this.deriveDropPolicyDescriptionFromChampionships(
scoringConfig.championships,
);
return {
leagueId: league.id, leagueId: league.id,
seasonId: activeSeason.id, seasonId: activeSeason.id,
gameId: game.id, gameId: game.id,
gameName: game.name, gameName: game.name,
scoringPresetId: presetId, scoringPresetId: presetId,
scoringPresetName: preset?.name, preset,
dropPolicySummary, championships: scoringConfig.championships,
championships,
}; };
}
private mapChampionship(championship: ChampionshipConfig): LeagueScoringChampionshipDTO { this.presenter.present(data);
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

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

View File

@@ -1,5 +1,5 @@
/** /**
* Query: GetPendingSponsorshipRequestsQuery * Application Use Case: GetPendingSponsorshipRequestsUseCase
* *
* Retrieves pending sponsorship requests for an entity owner to review. * 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 { ISponsorRepository } from '../../domain/repositories/ISponsorRepository';
import type { SponsorableEntityType } from '../../domain/entities/SponsorshipRequest'; import type { SponsorableEntityType } from '../../domain/entities/SponsorshipRequest';
import type { SponsorshipTier } from '../../domain/entities/SeasonSponsorship'; import type { SponsorshipTier } from '../../domain/entities/SeasonSponsorship';
import type { IPendingSponsorshipRequestsPresenter } from '../presenters/IPendingSponsorshipRequestsPresenter';
export interface GetPendingSponsorshipRequestsDTO { export interface GetPendingSponsorshipRequestsDTO {
entityType: SponsorableEntityType; entityType: SponsorableEntityType;
@@ -36,13 +37,14 @@ export interface GetPendingSponsorshipRequestsResultDTO {
totalCount: number; totalCount: number;
} }
export class GetPendingSponsorshipRequestsQuery { export class GetPendingSponsorshipRequestsUseCase {
constructor( constructor(
private readonly sponsorshipRequestRepo: ISponsorshipRequestRepository, private readonly sponsorshipRequestRepo: ISponsorshipRequestRepository,
private readonly sponsorRepo: ISponsorRepository, 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( const requests = await this.sponsorshipRequestRepo.findPendingByEntity(
dto.entityType, dto.entityType,
dto.entityId dto.entityId
@@ -72,11 +74,11 @@ export class GetPendingSponsorshipRequestsQuery {
// Sort by creation date (newest first) // Sort by creation date (newest first)
requestDTOs.sort((a, b) => b.createdAt.getTime() - a.createdAt.getTime()); requestDTOs.sort((a, b) => b.createdAt.getTime() - a.createdAt.getTime());
return { this.presenter.present({
entityType: dto.entityType, entityType: dto.entityType,
entityId: dto.entityId, entityId: dto.entityId,
requests: requestDTOs, requests: requestDTOs,
totalCount: requestDTOs.length, 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. * 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 { IPenaltyRepository } from '../../domain/repositories/IPenaltyRepository';
import type { IDriverRepository } from '../../domain/repositories/IDriverRepository'; import type { IDriverRepository } from '../../domain/repositories/IDriverRepository';
import type { PenaltyType, PenaltyStatus } from '../../domain/entities/Penalty'; import type { IRacePenaltiesPresenter } from '../presenters/IRacePenaltiesPresenter';
export interface RacePenaltyDTO { export class GetRacePenaltiesUseCase {
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 {
constructor( constructor(
private readonly penaltyRepository: IPenaltyRepository, private readonly penaltyRepository: IPenaltyRepository,
private readonly driverRepository: IDriverRepository, 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); const penalties = await this.penaltyRepository.findByRaceId(raceId);
// Load all driver details in parallel // Load all driver details in parallel
@@ -53,22 +37,6 @@ export class GetRacePenaltiesQuery {
} }
}); });
return penalties.map(penalty => ({ this.presenter.present(penalties, driverMap);
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,
}));
} }
} }

View File

@@ -1,38 +1,22 @@
/** /**
* Application Query: GetRaceProtestsQuery * Use Case: GetRaceProtestsUseCase
* *
* Returns all protests filed for a specific race, with driver details. * 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 { IProtestRepository } from '../../domain/repositories/IProtestRepository';
import type { IDriverRepository } from '../../domain/repositories/IDriverRepository'; import type { IDriverRepository } from '../../domain/repositories/IDriverRepository';
import type { ProtestStatus, ProtestIncident } from '../../domain/entities/Protest'; import type { IRaceProtestsPresenter } from '../presenters/IRaceProtestsPresenter';
export interface RaceProtestDTO { export class GetRaceProtestsUseCase {
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 {
constructor( constructor(
private readonly protestRepository: IProtestRepository, private readonly protestRepository: IProtestRepository,
private readonly driverRepository: IDriverRepository, 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); const protests = await this.protestRepository.findByRaceId(raceId);
// Load all driver details in parallel // Load all driver details in parallel
@@ -56,22 +40,6 @@ export class GetRaceProtestsQuery {
} }
}); });
return protests.map(protest => ({ this.presenter.present(protests, driverMap);
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(),
}));
} }
} }

View File

@@ -1,17 +1,22 @@
import type { IRaceRegistrationRepository } from '@gridpilot/racing/domain/repositories/IRaceRegistrationRepository'; import type { IRaceRegistrationRepository } from '@gridpilot/racing/domain/repositories/IRaceRegistrationRepository';
import type { GetRaceRegistrationsQueryParamsDTO } from '../dto/RaceRegistrationQueryDTO'; import type { GetRaceRegistrationsQueryParamsDTO } from '../dto/RaceRegistrationQueryDTO';
import type { IRaceRegistrationsPresenter } from '../presenters/IRaceRegistrationsPresenter';
/** /**
* Query object returning registered driver IDs for a race. * Use Case: GetRaceRegistrationsUseCase
* Mirrors legacy getRegisteredDrivers behavior. *
* Returns registered driver IDs for a race.
* Orchestrates domain logic and delegates presentation to the presenter.
*/ */
export class GetRaceRegistrationsQuery { export class GetRaceRegistrationsUseCase {
constructor( constructor(
private readonly registrationRepository: IRaceRegistrationRepository, private readonly registrationRepository: IRaceRegistrationRepository,
public readonly presenter: IRaceRegistrationsPresenter,
) {} ) {}
async execute(params: GetRaceRegistrationsQueryParamsDTO): Promise<string[]> { async execute(params: GetRaceRegistrationsQueryParamsDTO): Promise<void> {
const { raceId } = params; 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). * Returns race details enriched with calculated Strength of Field (SOF).
* SOF is calculated from participant ratings if not already stored on the race. * 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'; import type { IRaceRepository } from '../../domain/repositories/IRaceRepository';
@@ -13,18 +14,13 @@ import {
AverageStrengthOfFieldCalculator, AverageStrengthOfFieldCalculator,
type StrengthOfFieldCalculator, type StrengthOfFieldCalculator,
} from '../../domain/services/StrengthOfFieldCalculator'; } from '../../domain/services/StrengthOfFieldCalculator';
import type { RaceDTO } from '../dto/RaceDTO'; import type { IRaceWithSOFPresenter } from '../presenters/IRaceWithSOFPresenter';
export interface GetRaceWithSOFQueryParams { export interface GetRaceWithSOFQueryParams {
raceId: string; raceId: string;
} }
export interface RaceWithSOFDTO extends Omit<RaceDTO, 'strengthOfField'> { export class GetRaceWithSOFUseCase {
strengthOfField: number | null;
participantCount: number;
}
export class GetRaceWithSOFQuery {
private readonly sofCalculator: StrengthOfFieldCalculator; private readonly sofCalculator: StrengthOfFieldCalculator;
constructor( constructor(
@@ -32,12 +28,13 @@ export class GetRaceWithSOFQuery {
private readonly registrationRepository: IRaceRegistrationRepository, private readonly registrationRepository: IRaceRegistrationRepository,
private readonly resultRepository: IResultRepository, private readonly resultRepository: IResultRepository,
private readonly driverRatingProvider: DriverRatingProvider, private readonly driverRatingProvider: DriverRatingProvider,
public readonly presenter: IRaceWithSOFPresenter,
sofCalculator?: StrengthOfFieldCalculator, sofCalculator?: StrengthOfFieldCalculator,
) { ) {
this.sofCalculator = sofCalculator ?? new AverageStrengthOfFieldCalculator(); this.sofCalculator = sofCalculator ?? new AverageStrengthOfFieldCalculator();
} }
async execute(params: GetRaceWithSOFQueryParams): Promise<RaceWithSOFDTO | null> { async execute(params: GetRaceWithSOFQueryParams): Promise<void> {
const { raceId } = params; const { raceId } = params;
const race = await this.raceRepository.findById(raceId); const race = await this.raceRepository.findById(raceId);
@@ -69,20 +66,20 @@ export class GetRaceWithSOFQuery {
strengthOfField = this.sofCalculator.calculate(driverRatings); strengthOfField = this.sofCalculator.calculate(driverRatings);
} }
return { this.presenter.present(
id: race.id, race.id,
leagueId: race.leagueId, race.leagueId,
scheduledAt: race.scheduledAt.toISOString(), race.scheduledAt,
track: race.track, race.track,
trackId: race.trackId, race.trackId,
car: race.car, race.car,
carId: race.carId, race.carId,
sessionType: race.sessionType, race.sessionType,
status: race.status, race.status,
strengthOfField, strengthOfField,
registeredCount: race.registeredCount ?? participantIds.length, race.registeredCount ?? participantIds.length,
maxParticipants: race.maxParticipants, race.maxParticipants,
participantCount: participantIds.length, 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,5 +1,5 @@
/** /**
* Application Query: GetSponsorDashboardQuery * Application Use Case: GetSponsorDashboardUseCase
* *
* Returns sponsor dashboard metrics including sponsorships, impressions, and investment data. * 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 { ILeagueRepository } from '../../domain/repositories/ILeagueRepository';
import type { ILeagueMembershipRepository } from '../../domain/repositories/ILeagueMembershipRepository'; import type { ILeagueMembershipRepository } from '../../domain/repositories/ILeagueMembershipRepository';
import type { IRaceRepository } from '../../domain/repositories/IRaceRepository'; import type { IRaceRepository } from '../../domain/repositories/IRaceRepository';
import type { ISponsorDashboardPresenter } from '../presenters/ISponsorDashboardPresenter';
export interface GetSponsorDashboardQueryParams { export interface GetSponsorDashboardQueryParams {
sponsorId: string; sponsorId: string;
@@ -46,7 +47,7 @@ export interface SponsorDashboardDTO {
}; };
} }
export class GetSponsorDashboardQuery { export class GetSponsorDashboardUseCase {
constructor( constructor(
private readonly sponsorRepository: ISponsorRepository, private readonly sponsorRepository: ISponsorRepository,
private readonly seasonSponsorshipRepository: ISeasonSponsorshipRepository, private readonly seasonSponsorshipRepository: ISeasonSponsorshipRepository,
@@ -54,14 +55,16 @@ export class GetSponsorDashboardQuery {
private readonly leagueRepository: ILeagueRepository, private readonly leagueRepository: ILeagueRepository,
private readonly leagueMembershipRepository: ILeagueMembershipRepository, private readonly leagueMembershipRepository: ILeagueMembershipRepository,
private readonly raceRepository: IRaceRepository, 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 { sponsorId } = params;
const sponsor = await this.sponsorRepository.findById(sponsorId); const sponsor = await this.sponsorRepository.findById(sponsorId);
if (!sponsor) { if (!sponsor) {
return null; this.presenter.present(null);
return;
} }
// Get all sponsorships for this sponsor // Get all sponsorships for this sponsor
@@ -140,7 +143,7 @@ export class GetSponsorDashboardQuery {
? Math.min(100, (mainSponsorships * 30) + (sponsorships.length * 10)) ? Math.min(100, (mainSponsorships * 30) + (sponsorships.length * 10))
: 0; : 0;
return { this.presenter.present({
sponsorId, sponsorId,
sponsorName: sponsor.name, sponsorName: sponsor.name,
metrics: { metrics: {
@@ -159,6 +162,6 @@ export class GetSponsorDashboardQuery {
totalInvestment, totalInvestment,
costPerThousandViews: Math.round(costPerThousandViews * 100) / 100, costPerThousandViews: Math.round(costPerThousandViews * 100) / 100,
}, },
}; });
} }
} }

View File

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

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