wip
This commit is contained in:
@@ -4,7 +4,8 @@ import {
|
||||
type LeagueScheduleDTO,
|
||||
type LeagueSchedulePreviewDTO,
|
||||
} from '@gridpilot/racing/application';
|
||||
import { getPreviewLeagueScheduleQuery } from '@/lib/di-container';
|
||||
import { getPreviewLeagueScheduleUseCase } from '@/lib/di-container';
|
||||
import { LeagueSchedulePreviewPresenter } from '@/lib/presenters/LeagueSchedulePreviewPresenter';
|
||||
|
||||
interface RequestBody {
|
||||
seasonStartDate?: string;
|
||||
@@ -73,11 +74,16 @@ export async function POST(request: NextRequest) {
|
||||
|
||||
const schedule = toLeagueScheduleDTO(json);
|
||||
|
||||
const query = getPreviewLeagueScheduleQuery();
|
||||
const preview: LeagueSchedulePreviewDTO = await query.execute({
|
||||
const presenter = new LeagueSchedulePreviewPresenter();
|
||||
const useCase = getPreviewLeagueScheduleUseCase();
|
||||
useCase.execute({
|
||||
schedule,
|
||||
maxRounds: 10,
|
||||
});
|
||||
const preview = presenter.getData();
|
||||
if (!preview) {
|
||||
return NextResponse.json({ error: 'Failed to generate preview' }, { status: 500 });
|
||||
}
|
||||
|
||||
return NextResponse.json(preview, { status: 200 });
|
||||
} catch (error) {
|
||||
|
||||
@@ -1,7 +1,8 @@
|
||||
import { NextResponse } from 'next/server';
|
||||
import type { NextRequest } from 'next/server';
|
||||
import { cookies } from 'next/headers';
|
||||
import { getGetSponsorDashboardQuery } from '@/lib/di-container';
|
||||
import { getGetSponsorDashboardUseCase } from '@/lib/di-container';
|
||||
import { SponsorDashboardPresenter } from '@/lib/presenters/SponsorDashboardPresenter';
|
||||
|
||||
export async function GET(request: NextRequest) {
|
||||
try {
|
||||
@@ -16,8 +17,10 @@ export async function GET(request: NextRequest) {
|
||||
);
|
||||
}
|
||||
|
||||
const query = getGetSponsorDashboardQuery();
|
||||
const dashboard = await query.execute({ sponsorId });
|
||||
const presenter = new SponsorDashboardPresenter();
|
||||
const useCase = getGetSponsorDashboardUseCase();
|
||||
await useCase.execute({ sponsorId });
|
||||
const dashboard = presenter.getData();
|
||||
|
||||
if (!dashboard) {
|
||||
return NextResponse.json(
|
||||
|
||||
@@ -1,7 +1,8 @@
|
||||
import { NextResponse } from 'next/server';
|
||||
import type { NextRequest } from 'next/server';
|
||||
import { cookies } from 'next/headers';
|
||||
import { getGetSponsorSponsorshipsQuery } from '@/lib/di-container';
|
||||
import { getGetSponsorSponsorshipsUseCase } from '@/lib/di-container';
|
||||
import { SponsorSponsorshipsPresenter } from '@/lib/presenters/SponsorSponsorshipsPresenter';
|
||||
|
||||
export async function GET(request: NextRequest) {
|
||||
try {
|
||||
@@ -16,8 +17,10 @@ export async function GET(request: NextRequest) {
|
||||
);
|
||||
}
|
||||
|
||||
const query = getGetSponsorSponsorshipsQuery();
|
||||
const sponsorships = await query.execute({ sponsorId });
|
||||
const presenter = new SponsorSponsorshipsPresenter();
|
||||
const useCase = getGetSponsorSponsorshipsUseCase();
|
||||
await useCase.execute({ sponsorId });
|
||||
const sponsorships = presenter.getData();
|
||||
|
||||
if (!sponsorships) {
|
||||
return NextResponse.json(
|
||||
|
||||
@@ -40,11 +40,11 @@ import {
|
||||
getDriverRepository,
|
||||
getDriverStats,
|
||||
getAllDriverRankings,
|
||||
getGetDriverTeamQuery,
|
||||
getGetDriverTeamUseCase,
|
||||
getSocialRepository,
|
||||
getImageService,
|
||||
getGetAllTeamsQuery,
|
||||
getGetTeamMembersQuery,
|
||||
getGetAllTeamsUseCase,
|
||||
getGetTeamMembersUseCase,
|
||||
} from '@/lib/di-container';
|
||||
import { Driver, EntityMappers, type Team } from '@gridpilot/racing';
|
||||
import type { DriverDTO } from '@gridpilot/racing';
|
||||
@@ -382,18 +382,23 @@ export default function DriverDetailPage({
|
||||
setDriver(driverDto);
|
||||
|
||||
// Load team data
|
||||
const teamQuery = getGetDriverTeamQuery();
|
||||
const teamResult = await teamQuery.execute({ driverId });
|
||||
setTeamData(teamResult);
|
||||
const teamUseCase = getGetDriverTeamUseCase();
|
||||
await teamUseCase.execute({ driverId });
|
||||
const teamViewModel = teamUseCase.presenter.getViewModel();
|
||||
setTeamData(teamViewModel.result);
|
||||
|
||||
// Load ALL team memberships
|
||||
const allTeamsQuery = getGetAllTeamsQuery();
|
||||
const allTeams = await allTeamsQuery.execute();
|
||||
const membershipsQuery = getGetTeamMembersQuery();
|
||||
const allTeamsUseCase = getGetAllTeamsUseCase();
|
||||
await allTeamsUseCase.execute();
|
||||
const allTeamsViewModel = allTeamsUseCase.presenter.getViewModel();
|
||||
const allTeams = allTeamsViewModel.teams;
|
||||
const membershipsUseCase = getGetTeamMembersUseCase();
|
||||
|
||||
const memberships: TeamMembershipInfo[] = [];
|
||||
for (const team of allTeams) {
|
||||
const members = await membershipsQuery.execute({ teamId: team.id });
|
||||
await membershipsUseCase.execute({ teamId: team.id });
|
||||
const membersViewModel = membershipsUseCase.presenter.getViewModel();
|
||||
const members = membersViewModel.members;
|
||||
const membership = members.find((m) => m.driverId === driverId);
|
||||
if (membership) {
|
||||
memberships.push({
|
||||
|
||||
@@ -27,27 +27,15 @@ import Button from '@/components/ui/Button';
|
||||
import Input from '@/components/ui/Input';
|
||||
import Card from '@/components/ui/Card';
|
||||
import Heading from '@/components/ui/Heading';
|
||||
import { getDriverRepository, getDriverStats, getAllDriverRankings, getImageService } from '@/lib/di-container';
|
||||
import { getGetDriversLeaderboardUseCase } from '@/lib/di-container';
|
||||
import type { DriverLeaderboardItemViewModel, SkillLevel } from '@gridpilot/racing/application/presenters/IDriversLeaderboardPresenter';
|
||||
import Image from 'next/image';
|
||||
|
||||
// ============================================================================
|
||||
// TYPES
|
||||
// ============================================================================
|
||||
|
||||
type SkillLevel = 'beginner' | 'intermediate' | 'advanced' | 'pro';
|
||||
|
||||
interface DriverListItem {
|
||||
id: string;
|
||||
name: string;
|
||||
rating: number;
|
||||
skillLevel: SkillLevel;
|
||||
nationality: string;
|
||||
racesCompleted: number;
|
||||
wins: number;
|
||||
podiums: number;
|
||||
isActive: boolean;
|
||||
rank: number;
|
||||
}
|
||||
type DriverListItem = DriverLeaderboardItemViewModel;
|
||||
|
||||
// ============================================================================
|
||||
// DEMO DATA
|
||||
@@ -87,7 +75,6 @@ interface FeaturedDriverCardProps {
|
||||
}
|
||||
|
||||
function FeaturedDriverCard({ driver, position, onClick }: FeaturedDriverCardProps) {
|
||||
const imageService = getImageService();
|
||||
const levelConfig = SKILL_LEVELS.find((l) => l.id === driver.skillLevel);
|
||||
|
||||
const getBorderColor = (pos: number) => {
|
||||
@@ -131,7 +118,7 @@ function FeaturedDriverCard({ driver, position, onClick }: FeaturedDriverCardPro
|
||||
{/* Avatar & Name */}
|
||||
<div className="flex items-center gap-4 mb-4">
|
||||
<div className="relative w-16 h-16 rounded-full overflow-hidden border-2 border-charcoal-outline group-hover:border-primary-blue transition-colors">
|
||||
<Image src={imageService.getDriverAvatar(driver.id)} alt={driver.name} fill className="object-cover" />
|
||||
<Image src={driver.avatarUrl} alt={driver.name} fill className="object-cover" />
|
||||
</div>
|
||||
<div>
|
||||
<h3 className="text-lg font-semibold text-white group-hover:text-primary-blue transition-colors">
|
||||
@@ -236,7 +223,6 @@ interface LeaderboardPreviewProps {
|
||||
|
||||
function LeaderboardPreview({ drivers, onDriverClick }: LeaderboardPreviewProps) {
|
||||
const router = useRouter();
|
||||
const imageService = getImageService();
|
||||
const top5 = drivers.slice(0, 5);
|
||||
|
||||
const getMedalColor = (position: number) => {
|
||||
@@ -300,7 +286,7 @@ function LeaderboardPreview({ drivers, onDriverClick }: LeaderboardPreviewProps)
|
||||
|
||||
{/* Avatar */}
|
||||
<div className="relative w-9 h-9 rounded-full overflow-hidden border-2 border-charcoal-outline">
|
||||
<Image src={imageService.getDriverAvatar(driver.id)} alt={driver.name} fill className="object-cover" />
|
||||
<Image src={driver.avatarUrl} alt={driver.name} fill className="object-cover" />
|
||||
</div>
|
||||
|
||||
{/* Info */}
|
||||
@@ -345,7 +331,6 @@ interface RecentActivityProps {
|
||||
}
|
||||
|
||||
function RecentActivity({ drivers, onDriverClick }: RecentActivityProps) {
|
||||
const imageService = getImageService();
|
||||
const activeDrivers = drivers.filter((d) => d.isActive).slice(0, 6);
|
||||
|
||||
return (
|
||||
@@ -371,7 +356,7 @@ function RecentActivity({ drivers, onDriverClick }: RecentActivityProps) {
|
||||
className="p-3 rounded-xl bg-iron-gray/40 border border-charcoal-outline hover:border-performance-green/40 transition-all group text-center"
|
||||
>
|
||||
<div className="relative w-12 h-12 mx-auto rounded-full overflow-hidden border-2 border-charcoal-outline mb-2">
|
||||
<Image src={imageService.getDriverAvatar(driver.id)} alt={driver.name} fill className="object-cover" />
|
||||
<Image src={driver.avatarUrl} alt={driver.name} fill className="object-cover" />
|
||||
<div className="absolute bottom-0 right-0 w-3 h-3 rounded-full bg-performance-green border-2 border-iron-gray" />
|
||||
</div>
|
||||
<p className="text-sm font-medium text-white truncate group-hover:text-performance-green transition-colors">
|
||||
@@ -395,57 +380,20 @@ export default function DriversPage() {
|
||||
const [drivers, setDrivers] = useState<DriverListItem[]>([]);
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [searchQuery, setSearchQuery] = useState('');
|
||||
const [totalRaces, setTotalRaces] = useState(0);
|
||||
const [totalWins, setTotalWins] = useState(0);
|
||||
const [activeCount, setActiveCount] = useState(0);
|
||||
|
||||
useEffect(() => {
|
||||
const load = async () => {
|
||||
const driverRepo = getDriverRepository();
|
||||
const allDrivers = await driverRepo.findAll();
|
||||
const rankings = getAllDriverRankings();
|
||||
const useCase = getGetDriversLeaderboardUseCase();
|
||||
await useCase.execute();
|
||||
const viewModel = useCase.presenter.getViewModel();
|
||||
|
||||
const items: DriverListItem[] = allDrivers.map((driver) => {
|
||||
const stats = getDriverStats(driver.id);
|
||||
const rating = stats?.rating ?? 0;
|
||||
const wins = stats?.wins ?? 0;
|
||||
const podiums = stats?.podiums ?? 0;
|
||||
const totalRaces = stats?.totalRaces ?? 0;
|
||||
|
||||
let effectiveRank = Number.POSITIVE_INFINITY;
|
||||
if (typeof stats?.overallRank === 'number' && stats.overallRank > 0) {
|
||||
effectiveRank = stats.overallRank;
|
||||
} else {
|
||||
const indexInGlobal = rankings.findIndex((entry) => entry.driverId === driver.id);
|
||||
if (indexInGlobal !== -1) {
|
||||
effectiveRank = indexInGlobal + 1;
|
||||
}
|
||||
}
|
||||
|
||||
const skillLevel: SkillLevel =
|
||||
rating >= 3000 ? 'pro' : rating >= 2500 ? 'advanced' : rating >= 1800 ? 'intermediate' : 'beginner';
|
||||
|
||||
const isActive = rankings.some((r) => r.driverId === driver.id);
|
||||
|
||||
return {
|
||||
id: driver.id,
|
||||
name: driver.name,
|
||||
rating,
|
||||
skillLevel,
|
||||
nationality: driver.country,
|
||||
racesCompleted: totalRaces,
|
||||
wins,
|
||||
podiums,
|
||||
isActive,
|
||||
rank: effectiveRank,
|
||||
};
|
||||
});
|
||||
|
||||
// Sort by rank
|
||||
items.sort((a, b) => {
|
||||
const rankA = Number.isFinite(a.rank) && a.rank > 0 ? a.rank : Number.POSITIVE_INFINITY;
|
||||
const rankB = Number.isFinite(b.rank) && b.rank > 0 ? b.rank : Number.POSITIVE_INFINITY;
|
||||
return rankA - rankB || b.rating - a.rating;
|
||||
});
|
||||
|
||||
setDrivers(items);
|
||||
setDrivers(viewModel.drivers);
|
||||
setTotalRaces(viewModel.totalRaces);
|
||||
setTotalWins(viewModel.totalWins);
|
||||
setActiveCount(viewModel.activeCount);
|
||||
setLoading(false);
|
||||
};
|
||||
|
||||
@@ -465,10 +413,6 @@ export default function DriversPage() {
|
||||
);
|
||||
});
|
||||
|
||||
// Stats
|
||||
const totalRaces = drivers.reduce((sum, d) => sum + d.racesCompleted, 0);
|
||||
const totalWins = drivers.reduce((sum, d) => sum + d.wins, 0);
|
||||
const activeCount = drivers.filter((d) => d.isActive).length;
|
||||
|
||||
// Featured drivers (top 4)
|
||||
const featuredDrivers = filteredDrivers.slice(0, 4);
|
||||
|
||||
@@ -19,27 +19,17 @@ import {
|
||||
import Button from '@/components/ui/Button';
|
||||
import Input from '@/components/ui/Input';
|
||||
import Heading from '@/components/ui/Heading';
|
||||
import { getDriverRepository, getDriverStats, getAllDriverRankings, getImageService } from '@/lib/di-container';
|
||||
import { getGetDriversLeaderboardUseCase } from '@/lib/di-container';
|
||||
import type { DriverLeaderboardItemViewModel, SkillLevel } from '@gridpilot/racing/application/presenters/IDriversLeaderboardPresenter';
|
||||
import Image from 'next/image';
|
||||
|
||||
// ============================================================================
|
||||
// TYPES
|
||||
// ============================================================================
|
||||
|
||||
type SkillLevel = 'beginner' | 'intermediate' | 'advanced' | 'pro';
|
||||
type SortBy = 'rank' | 'rating' | 'wins' | 'podiums' | 'winRate';
|
||||
|
||||
interface DriverListItem {
|
||||
id: string;
|
||||
name: string;
|
||||
rating: number;
|
||||
skillLevel: SkillLevel;
|
||||
nationality: string;
|
||||
racesCompleted: number;
|
||||
wins: number;
|
||||
podiums: number;
|
||||
rank: number;
|
||||
}
|
||||
type DriverListItem = DriverLeaderboardItemViewModel;
|
||||
|
||||
// ============================================================================
|
||||
// SKILL LEVEL CONFIG
|
||||
@@ -81,7 +71,6 @@ interface TopThreePodiumProps {
|
||||
}
|
||||
|
||||
function TopThreePodium({ drivers, onDriverClick }: TopThreePodiumProps) {
|
||||
const imageService = getImageService();
|
||||
const top3 = drivers.slice(0, 3);
|
||||
|
||||
if (top3.length < 3) return null;
|
||||
@@ -122,7 +111,7 @@ function TopThreePodium({ drivers, onDriverClick }: TopThreePodiumProps) {
|
||||
{/* Avatar */}
|
||||
<div className={`relative ${position === 1 ? 'w-24 h-24 lg:w-28 lg:h-28' : 'w-20 h-20 lg:w-24 lg:h-24'} rounded-full overflow-hidden border-4 ${position === 1 ? 'border-yellow-400 shadow-[0_0_30px_rgba(250,204,21,0.3)]' : position === 2 ? 'border-gray-300' : 'border-amber-600'} group-hover:scale-105 transition-transform`}>
|
||||
<Image
|
||||
src={imageService.getDriverAvatar(driver.id)}
|
||||
src={driver.avatarUrl}
|
||||
alt={driver.name}
|
||||
fill
|
||||
className="object-cover"
|
||||
@@ -178,7 +167,6 @@ function TopThreePodium({ drivers, onDriverClick }: TopThreePodiumProps) {
|
||||
|
||||
export default function DriverLeaderboardPage() {
|
||||
const router = useRouter();
|
||||
const imageService = getImageService();
|
||||
const [drivers, setDrivers] = useState<DriverListItem[]>([]);
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [searchQuery, setSearchQuery] = useState('');
|
||||
@@ -188,44 +176,10 @@ export default function DriverLeaderboardPage() {
|
||||
|
||||
useEffect(() => {
|
||||
const load = async () => {
|
||||
const driverRepo = getDriverRepository();
|
||||
const allDrivers = await driverRepo.findAll();
|
||||
const rankings = getAllDriverRankings();
|
||||
|
||||
const items: DriverListItem[] = allDrivers.map((driver) => {
|
||||
const stats = getDriverStats(driver.id);
|
||||
const rating = stats?.rating ?? 0;
|
||||
const wins = stats?.wins ?? 0;
|
||||
const podiums = stats?.podiums ?? 0;
|
||||
const totalRaces = stats?.totalRaces ?? 0;
|
||||
|
||||
let effectiveRank = Number.POSITIVE_INFINITY;
|
||||
if (typeof stats?.overallRank === 'number' && stats.overallRank > 0) {
|
||||
effectiveRank = stats.overallRank;
|
||||
} else {
|
||||
const indexInGlobal = rankings.findIndex((entry) => entry.driverId === driver.id);
|
||||
if (indexInGlobal !== -1) {
|
||||
effectiveRank = indexInGlobal + 1;
|
||||
}
|
||||
}
|
||||
|
||||
const skillLevel: SkillLevel =
|
||||
rating >= 3000 ? 'pro' : rating >= 2500 ? 'advanced' : rating >= 1800 ? 'intermediate' : 'beginner';
|
||||
|
||||
return {
|
||||
id: driver.id,
|
||||
name: driver.name,
|
||||
rating,
|
||||
skillLevel,
|
||||
nationality: driver.country,
|
||||
racesCompleted: totalRaces,
|
||||
wins,
|
||||
podiums,
|
||||
rank: effectiveRank,
|
||||
};
|
||||
});
|
||||
|
||||
setDrivers(items);
|
||||
const useCase = getGetDriversLeaderboardUseCase();
|
||||
await useCase.execute();
|
||||
const viewModel = useCase.presenter.getViewModel();
|
||||
setDrivers(viewModel.drivers);
|
||||
setLoading(false);
|
||||
};
|
||||
|
||||
@@ -443,7 +397,7 @@ export default function DriverLeaderboardPage() {
|
||||
{/* Driver Info */}
|
||||
<div className="col-span-5 lg:col-span-4 flex items-center gap-3">
|
||||
<div className="relative w-10 h-10 rounded-full overflow-hidden border-2 border-charcoal-outline">
|
||||
<Image src={imageService.getDriverAvatar(driver.id)} alt={driver.name} fill className="object-cover" />
|
||||
<Image src={driver.avatarUrl} alt={driver.name} fill className="object-cover" />
|
||||
</div>
|
||||
<div className="min-w-0">
|
||||
<p className="text-white font-semibold truncate group-hover:text-primary-blue transition-colors">
|
||||
|
||||
@@ -20,36 +20,18 @@ import {
|
||||
} from 'lucide-react';
|
||||
import Button from '@/components/ui/Button';
|
||||
import Heading from '@/components/ui/Heading';
|
||||
import { getDriverRepository, getDriverStats, getAllDriverRankings, getImageService, getGetAllTeamsQuery, getGetTeamMembersQuery } from '@/lib/di-container';
|
||||
import { getGetDriversLeaderboardUseCase, getGetTeamsLeaderboardUseCase } from '@/lib/di-container';
|
||||
import type { DriverLeaderboardItemViewModel, SkillLevel } from '@gridpilot/racing/application/presenters/IDriversLeaderboardPresenter';
|
||||
import type { TeamLeaderboardItemViewModel } from '@gridpilot/racing/application/presenters/ITeamsLeaderboardPresenter';
|
||||
import Image from 'next/image';
|
||||
import type { Team } from '@gridpilot/racing';
|
||||
|
||||
// ============================================================================
|
||||
// TYPES
|
||||
// ============================================================================
|
||||
|
||||
type SkillLevel = 'beginner' | 'intermediate' | 'advanced' | 'pro';
|
||||
type DriverListItem = DriverLeaderboardItemViewModel;
|
||||
|
||||
interface DriverListItem {
|
||||
id: string;
|
||||
name: string;
|
||||
rating: number;
|
||||
skillLevel: SkillLevel;
|
||||
nationality: string;
|
||||
wins: number;
|
||||
podiums: number;
|
||||
rank: number;
|
||||
}
|
||||
|
||||
interface TeamDisplayData {
|
||||
id: string;
|
||||
name: string;
|
||||
memberCount: number;
|
||||
rating: number | null;
|
||||
totalWins: number;
|
||||
totalRaces: number;
|
||||
performanceLevel: SkillLevel;
|
||||
}
|
||||
type TeamDisplayData = TeamLeaderboardItemViewModel;
|
||||
|
||||
// ============================================================================
|
||||
// SKILL LEVEL CONFIG
|
||||
@@ -80,7 +62,6 @@ interface DriverLeaderboardPreviewProps {
|
||||
|
||||
function DriverLeaderboardPreview({ drivers, onDriverClick }: DriverLeaderboardPreviewProps) {
|
||||
const router = useRouter();
|
||||
const imageService = getImageService();
|
||||
const top10 = drivers.slice(0, 10);
|
||||
|
||||
const getMedalColor = (position: number) => {
|
||||
@@ -144,7 +125,7 @@ function DriverLeaderboardPreview({ drivers, onDriverClick }: DriverLeaderboardP
|
||||
|
||||
{/* Avatar */}
|
||||
<div className="relative w-9 h-9 rounded-full overflow-hidden border-2 border-charcoal-outline">
|
||||
<Image src={imageService.getDriverAvatar(driver.id)} alt={driver.name} fill className="object-cover" />
|
||||
<Image src={driver.avatarUrl} alt={driver.name} fill className="object-cover" />
|
||||
</div>
|
||||
|
||||
{/* Info */}
|
||||
@@ -189,7 +170,6 @@ interface TeamLeaderboardPreviewProps {
|
||||
|
||||
function TeamLeaderboardPreview({ teams, onTeamClick }: TeamLeaderboardPreviewProps) {
|
||||
const router = useRouter();
|
||||
const imageService = getImageService();
|
||||
const top5 = [...teams]
|
||||
.filter((t) => t.rating !== null)
|
||||
.sort((a, b) => (b.rating ?? 0) - (a.rating ?? 0))
|
||||
@@ -304,99 +284,16 @@ export default function LeaderboardsPage() {
|
||||
useEffect(() => {
|
||||
const load = async () => {
|
||||
try {
|
||||
// Load drivers
|
||||
const driverRepo = getDriverRepository();
|
||||
const allDrivers = await driverRepo.findAll();
|
||||
const rankings = getAllDriverRankings();
|
||||
const driversUseCase = getGetDriversLeaderboardUseCase();
|
||||
const teamsUseCase = getGetTeamsLeaderboardUseCase();
|
||||
await driversUseCase.execute();
|
||||
await teamsUseCase.execute();
|
||||
|
||||
const driverItems: DriverListItem[] = allDrivers.map((driver) => {
|
||||
const stats = getDriverStats(driver.id);
|
||||
const rating = stats?.rating ?? 0;
|
||||
const wins = stats?.wins ?? 0;
|
||||
const podiums = stats?.podiums ?? 0;
|
||||
const driversViewModel = driversUseCase.presenter.getViewModel();
|
||||
const teamsViewModel = teamsUseCase.presenter.getViewModel();
|
||||
|
||||
let effectiveRank = Number.POSITIVE_INFINITY;
|
||||
if (typeof stats?.overallRank === 'number' && stats.overallRank > 0) {
|
||||
effectiveRank = stats.overallRank;
|
||||
} else {
|
||||
const indexInGlobal = rankings.findIndex((entry) => entry.driverId === driver.id);
|
||||
if (indexInGlobal !== -1) {
|
||||
effectiveRank = indexInGlobal + 1;
|
||||
}
|
||||
}
|
||||
|
||||
const skillLevel: SkillLevel =
|
||||
rating >= 3000 ? 'pro' : rating >= 2500 ? 'advanced' : rating >= 1800 ? 'intermediate' : 'beginner';
|
||||
|
||||
return {
|
||||
id: driver.id,
|
||||
name: driver.name,
|
||||
rating,
|
||||
skillLevel,
|
||||
nationality: driver.country,
|
||||
wins,
|
||||
podiums,
|
||||
rank: effectiveRank,
|
||||
};
|
||||
});
|
||||
|
||||
// Sort by rank
|
||||
driverItems.sort((a, b) => {
|
||||
const rankA = Number.isFinite(a.rank) && a.rank > 0 ? a.rank : Number.POSITIVE_INFINITY;
|
||||
const rankB = Number.isFinite(b.rank) && b.rank > 0 ? b.rank : Number.POSITIVE_INFINITY;
|
||||
return rankA - rankB || b.rating - a.rating;
|
||||
});
|
||||
|
||||
// Load teams
|
||||
const allTeamsQuery = getGetAllTeamsQuery();
|
||||
const teamMembersQuery = getGetTeamMembersQuery();
|
||||
const allTeams = await allTeamsQuery.execute();
|
||||
const teamData: TeamDisplayData[] = [];
|
||||
|
||||
await Promise.all(
|
||||
allTeams.map(async (team: Team) => {
|
||||
const memberships = await teamMembersQuery.execute({ teamId: team.id });
|
||||
const memberCount = memberships.length;
|
||||
|
||||
let ratingSum = 0;
|
||||
let ratingCount = 0;
|
||||
let totalWins = 0;
|
||||
let totalRaces = 0;
|
||||
|
||||
for (const membership of memberships) {
|
||||
const stats = getDriverStats(membership.driverId);
|
||||
if (!stats) continue;
|
||||
if (typeof stats.rating === 'number') {
|
||||
ratingSum += stats.rating;
|
||||
ratingCount += 1;
|
||||
}
|
||||
totalWins += stats.wins ?? 0;
|
||||
totalRaces += stats.totalRaces ?? 0;
|
||||
}
|
||||
|
||||
const averageRating = ratingCount > 0 ? ratingSum / ratingCount : null;
|
||||
|
||||
let performanceLevel: SkillLevel = 'beginner';
|
||||
if (averageRating !== null) {
|
||||
if (averageRating >= 4500) performanceLevel = 'pro';
|
||||
else if (averageRating >= 3000) performanceLevel = 'advanced';
|
||||
else if (averageRating >= 2000) performanceLevel = 'intermediate';
|
||||
}
|
||||
|
||||
teamData.push({
|
||||
id: team.id,
|
||||
name: team.name,
|
||||
memberCount,
|
||||
rating: averageRating,
|
||||
totalWins,
|
||||
totalRaces,
|
||||
performanceLevel,
|
||||
});
|
||||
}),
|
||||
);
|
||||
|
||||
setDrivers(driverItems);
|
||||
setTeams(teamData);
|
||||
setDrivers(driversViewModel.drivers);
|
||||
setTeams(teamsViewModel.teams);
|
||||
} catch (error) {
|
||||
console.error('Failed to load leaderboard data:', error);
|
||||
setDrivers([]);
|
||||
|
||||
@@ -25,10 +25,10 @@ import {
|
||||
getLeagueRepository,
|
||||
getRaceRepository,
|
||||
getDriverRepository,
|
||||
getGetLeagueScoringConfigQuery,
|
||||
getGetLeagueScoringConfigUseCase,
|
||||
getDriverStats,
|
||||
getAllDriverRankings,
|
||||
getGetLeagueStatsQuery,
|
||||
getGetLeagueStatsUseCase,
|
||||
getSeasonRepository,
|
||||
getSponsorRepository,
|
||||
getSeasonSponsorshipRepository,
|
||||
@@ -104,7 +104,7 @@ export default function LeagueDetailPage() {
|
||||
const leagueRepo = getLeagueRepository();
|
||||
const raceRepo = getRaceRepository();
|
||||
const driverRepo = getDriverRepository();
|
||||
const leagueStatsQuery = getGetLeagueStatsQuery();
|
||||
const leagueStatsUseCase = getGetLeagueStatsUseCase();
|
||||
const seasonRepo = getSeasonRepository();
|
||||
const sponsorRepo = getSponsorRepository();
|
||||
const sponsorshipRepo = getSeasonSponsorshipRepository();
|
||||
@@ -124,9 +124,10 @@ export default function LeagueDetailPage() {
|
||||
setOwner(ownerData);
|
||||
|
||||
// Load scoring configuration for the active season
|
||||
const getLeagueScoringConfigQuery = getGetLeagueScoringConfigQuery();
|
||||
const scoring = await getLeagueScoringConfigQuery.execute({ leagueId });
|
||||
setScoringConfig(scoring);
|
||||
const getLeagueScoringConfigUseCase = getGetLeagueScoringConfigUseCase();
|
||||
await getLeagueScoringConfigUseCase.execute({ leagueId });
|
||||
const scoringViewModel = getLeagueScoringConfigUseCase.presenter.getViewModel();
|
||||
setScoringConfig(scoringViewModel);
|
||||
|
||||
// Load all drivers for standings and map to DTOs for UI components
|
||||
const allDrivers = await driverRepo.findAll();
|
||||
@@ -136,11 +137,12 @@ export default function LeagueDetailPage() {
|
||||
|
||||
setDrivers(driverDtos);
|
||||
|
||||
// Load league stats including average SOF from application query
|
||||
const leagueStats = await leagueStatsQuery.execute({ leagueId });
|
||||
if (leagueStats) {
|
||||
setAverageSOF(leagueStats.averageSOF);
|
||||
setCompletedRacesCount(leagueStats.completedRaces);
|
||||
// Load league stats including average SOF from application use case
|
||||
await leagueStatsUseCase.execute({ leagueId });
|
||||
const leagueStatsViewModel = leagueStatsUseCase.presenter.getViewModel();
|
||||
if (leagueStatsViewModel) {
|
||||
setAverageSOF(leagueStatsViewModel.averageSOF);
|
||||
setCompletedRacesCount(leagueStatsViewModel.completedRaces);
|
||||
} else {
|
||||
// Fallback: count completed races manually
|
||||
const leagueRaces = await raceRepo.findByLeagueId(leagueId);
|
||||
|
||||
@@ -5,7 +5,7 @@ import { useParams } from 'next/navigation';
|
||||
import Card from '@/components/ui/Card';
|
||||
import {
|
||||
getLeagueRepository,
|
||||
getGetLeagueScoringConfigQuery
|
||||
getGetLeagueScoringConfigUseCase
|
||||
} from '@/lib/di-container';
|
||||
import type { LeagueScoringConfigDTO } from '@gridpilot/racing/application/dto/LeagueScoringConfigDTO';
|
||||
import type { League } from '@gridpilot/racing/domain/entities/League';
|
||||
@@ -25,7 +25,7 @@ export default function LeagueRulebookPage() {
|
||||
async function loadData() {
|
||||
try {
|
||||
const leagueRepo = getLeagueRepository();
|
||||
const scoringQuery = getGetLeagueScoringConfigQuery();
|
||||
const scoringUseCase = getGetLeagueScoringConfigUseCase();
|
||||
|
||||
const leagueData = await leagueRepo.findById(leagueId);
|
||||
if (!leagueData) {
|
||||
@@ -35,8 +35,9 @@ export default function LeagueRulebookPage() {
|
||||
|
||||
setLeague(leagueData);
|
||||
|
||||
const scoring = await scoringQuery.execute({ leagueId });
|
||||
setScoringConfig(scoring);
|
||||
await scoringUseCase.execute({ leagueId });
|
||||
const scoringViewModel = scoringUseCase.presenter.getViewModel();
|
||||
setScoringConfig(scoringViewModel);
|
||||
} catch (err) {
|
||||
console.error('Failed to load scoring config:', err);
|
||||
} finally {
|
||||
|
||||
@@ -7,11 +7,11 @@ import Button from '@/components/ui/Button';
|
||||
import {
|
||||
getLeagueRepository,
|
||||
getDriverRepository,
|
||||
getGetLeagueFullConfigQuery,
|
||||
getGetLeagueFullConfigUseCase,
|
||||
getLeagueMembershipRepository,
|
||||
getDriverStats,
|
||||
getAllDriverRankings,
|
||||
getListLeagueScoringPresetsQuery,
|
||||
getListLeagueScoringPresetsUseCase,
|
||||
getTransferLeagueOwnershipUseCase
|
||||
} from '@/lib/di-container';
|
||||
import { useEffectiveDriverId } from '@/lib/currentDriver';
|
||||
@@ -59,8 +59,8 @@ export default function LeagueSettingsPage() {
|
||||
try {
|
||||
const leagueRepo = getLeagueRepository();
|
||||
const driverRepo = getDriverRepository();
|
||||
const query = getGetLeagueFullConfigQuery();
|
||||
const presetsQuery = getListLeagueScoringPresetsQuery();
|
||||
const useCase = getGetLeagueFullConfigUseCase();
|
||||
const presetsUseCase = getListLeagueScoringPresetsUseCase();
|
||||
|
||||
const leagueData = await leagueRepo.findById(leagueId);
|
||||
if (!leagueData) {
|
||||
@@ -70,11 +70,13 @@ export default function LeagueSettingsPage() {
|
||||
|
||||
setLeague(leagueData);
|
||||
|
||||
const form = await query.execute({ leagueId });
|
||||
setConfigForm(form);
|
||||
await useCase.execute({ leagueId });
|
||||
const configViewModel = useCase.presenter.getViewModel();
|
||||
setConfigForm(configViewModel);
|
||||
|
||||
const presetsData = await presetsQuery.execute();
|
||||
setPresets(presetsData);
|
||||
await presetsUseCase.execute();
|
||||
const presetsViewModel = presetsUseCase.presenter.getViewModel();
|
||||
setPresets(presetsViewModel);
|
||||
|
||||
const entity = await driverRepo.findById(leagueData.ownerId);
|
||||
if (entity) {
|
||||
|
||||
@@ -10,7 +10,7 @@ import {
|
||||
type LeagueDriverSeasonStatsDTO,
|
||||
} from '@gridpilot/racing';
|
||||
import {
|
||||
getGetLeagueDriverSeasonStatsQuery,
|
||||
getGetLeagueDriverSeasonStatsUseCase,
|
||||
getDriverRepository,
|
||||
getLeagueMembershipRepository
|
||||
} from '@/lib/di-container';
|
||||
@@ -32,12 +32,13 @@ export default function LeagueStandingsPage() {
|
||||
|
||||
const loadData = useCallback(async () => {
|
||||
try {
|
||||
const getLeagueDriverSeasonStatsQuery = getGetLeagueDriverSeasonStatsQuery();
|
||||
const getLeagueDriverSeasonStatsUseCase = getGetLeagueDriverSeasonStatsUseCase();
|
||||
const driverRepo = getDriverRepository();
|
||||
const membershipRepo = getLeagueMembershipRepository();
|
||||
|
||||
const leagueStandings = await getLeagueDriverSeasonStatsQuery.execute({ leagueId });
|
||||
setStandings(leagueStandings);
|
||||
await getLeagueDriverSeasonStatsUseCase.execute({ leagueId });
|
||||
const standingsViewModel = getLeagueDriverSeasonStatsUseCase.presenter.getViewModel();
|
||||
setStandings(standingsViewModel);
|
||||
|
||||
const allDrivers = await driverRepo.findAll();
|
||||
const driverDtos: DriverDTO[] = allDrivers
|
||||
|
||||
@@ -31,7 +31,7 @@ import Card from '@/components/ui/Card';
|
||||
import Input from '@/components/ui/Input';
|
||||
import Heading from '@/components/ui/Heading';
|
||||
import type { LeagueSummaryDTO } from '@gridpilot/racing/application/dto/LeagueSummaryDTO';
|
||||
import { getGetAllLeaguesWithCapacityAndScoringQuery } from '@/lib/di-container';
|
||||
import { getGetAllLeaguesWithCapacityAndScoringUseCase } from '@/lib/di-container';
|
||||
|
||||
// ============================================================================
|
||||
// TYPES
|
||||
@@ -389,9 +389,10 @@ export default function LeaguesPage() {
|
||||
|
||||
const loadLeagues = async () => {
|
||||
try {
|
||||
const query = getGetAllLeaguesWithCapacityAndScoringQuery();
|
||||
const allLeagues = await query.execute();
|
||||
setRealLeagues(allLeagues);
|
||||
const useCase = getGetAllLeaguesWithCapacityAndScoringUseCase();
|
||||
await useCase.execute();
|
||||
const viewModel = useCase.presenter.getViewModel();
|
||||
setRealLeagues(viewModel);
|
||||
} catch (error) {
|
||||
console.error('Failed to load leagues:', error);
|
||||
} finally {
|
||||
|
||||
@@ -38,11 +38,11 @@ import {
|
||||
getDriverRepository,
|
||||
getDriverStats,
|
||||
getAllDriverRankings,
|
||||
getGetDriverTeamQuery,
|
||||
getGetDriverTeamUseCase,
|
||||
getSocialRepository,
|
||||
getImageService,
|
||||
getGetAllTeamsQuery,
|
||||
getGetTeamMembersQuery,
|
||||
getGetAllTeamsUseCase,
|
||||
getGetTeamMembersUseCase,
|
||||
} from '@/lib/di-container';
|
||||
import { Driver, EntityMappers, type DriverDTO, type Team } from '@gridpilot/racing';
|
||||
import CreateDriverForm from '@/components/drivers/CreateDriverForm';
|
||||
@@ -381,18 +381,23 @@ export default function ProfilePage() {
|
||||
setDriver(driverData);
|
||||
|
||||
// Load primary team data
|
||||
const teamQuery = getGetDriverTeamQuery();
|
||||
const teamResult = await teamQuery.execute({ driverId: currentDriverId });
|
||||
setTeamData(teamResult);
|
||||
const teamUseCase = getGetDriverTeamUseCase();
|
||||
await teamUseCase.execute({ driverId: currentDriverId });
|
||||
const teamViewModel = teamUseCase.presenter.getViewModel();
|
||||
setTeamData(teamViewModel.result);
|
||||
|
||||
// Load ALL team memberships
|
||||
const allTeamsQuery = getGetAllTeamsQuery();
|
||||
const allTeams = await allTeamsQuery.execute();
|
||||
const membershipsQuery = getGetTeamMembersQuery();
|
||||
const allTeamsUseCase = getGetAllTeamsUseCase();
|
||||
await allTeamsUseCase.execute();
|
||||
const allTeamsViewModel = allTeamsUseCase.presenter.getViewModel();
|
||||
const allTeams = allTeamsViewModel.teams;
|
||||
const membershipsUseCase = getGetTeamMembersUseCase();
|
||||
|
||||
const memberships: TeamMembershipInfo[] = [];
|
||||
for (const team of allTeams) {
|
||||
const members = await membershipsQuery.execute({ teamId: team.id });
|
||||
await membershipsUseCase.execute({ teamId: team.id });
|
||||
const membersViewModel = membershipsUseCase.presenter.getViewModel();
|
||||
const members = membersViewModel.members;
|
||||
const membership = members.find((m) => m.driverId === currentDriverId);
|
||||
if (membership) {
|
||||
memberships.push({
|
||||
|
||||
@@ -7,7 +7,7 @@ import Button from '@/components/ui/Button';
|
||||
import Breadcrumbs from '@/components/layout/Breadcrumbs';
|
||||
import PendingSponsorshipRequests, { type PendingRequestDTO } from '@/components/sponsors/PendingSponsorshipRequests';
|
||||
import {
|
||||
getGetPendingSponsorshipRequestsQuery,
|
||||
getGetPendingSponsorshipRequestsUseCase,
|
||||
getAcceptSponsorshipRequestUseCase,
|
||||
getRejectSponsorshipRequestUseCase,
|
||||
getDriverRepository,
|
||||
@@ -46,7 +46,7 @@ export default function SponsorshipRequestsPage() {
|
||||
const teamRepo = getTeamRepository();
|
||||
const leagueMembershipRepo = getLeagueMembershipRepository();
|
||||
const teamMembershipRepo = getTeamMembershipRepository();
|
||||
const query = getGetPendingSponsorshipRequestsQuery();
|
||||
const useCase = getGetPendingSponsorshipRequestsUseCase();
|
||||
|
||||
const allSections: EntitySection[] = [];
|
||||
|
||||
|
||||
@@ -17,11 +17,11 @@ import {
|
||||
getRaceRepository,
|
||||
getLeagueRepository,
|
||||
getDriverRepository,
|
||||
getGetRaceRegistrationsQuery,
|
||||
getIsDriverRegisteredForRaceQuery,
|
||||
getGetRaceRegistrationsUseCase,
|
||||
getIsDriverRegisteredForRaceUseCase,
|
||||
getRegisterForRaceUseCase,
|
||||
getWithdrawFromRaceUseCase,
|
||||
getGetRaceWithSOFQuery,
|
||||
getGetRaceWithSOFUseCase,
|
||||
getResultRepository,
|
||||
getImageService,
|
||||
} from '@/lib/di-container';
|
||||
@@ -80,7 +80,7 @@ export default function RaceDetailPage() {
|
||||
try {
|
||||
const raceRepo = getRaceRepository();
|
||||
const leagueRepo = getLeagueRepository();
|
||||
const raceWithSOFQuery = getGetRaceWithSOFQuery();
|
||||
const raceWithSOFUseCase = getGetRaceWithSOFUseCase();
|
||||
|
||||
const raceData = await raceRepo.findById(raceId);
|
||||
|
||||
@@ -92,10 +92,11 @@ export default function RaceDetailPage() {
|
||||
|
||||
setRace(raceData);
|
||||
|
||||
// Load race with SOF from application query
|
||||
const raceWithSOF = await raceWithSOFQuery.execute({ raceId });
|
||||
if (raceWithSOF) {
|
||||
setRaceSOF(raceWithSOF.strengthOfField);
|
||||
// Load race with SOF from application use case
|
||||
await raceWithSOFUseCase.execute({ raceId });
|
||||
const raceViewModel = raceWithSOFUseCase.presenter.getViewModel();
|
||||
if (raceViewModel) {
|
||||
setRaceSOF(raceViewModel.strengthOfField);
|
||||
}
|
||||
|
||||
// Load league data
|
||||
@@ -135,8 +136,10 @@ export default function RaceDetailPage() {
|
||||
try {
|
||||
const driverRepo = getDriverRepository();
|
||||
|
||||
const raceRegistrationsQuery = getGetRaceRegistrationsQuery();
|
||||
const registeredDriverIds = await raceRegistrationsQuery.execute({ raceId });
|
||||
const raceRegistrationsUseCase = getGetRaceRegistrationsUseCase();
|
||||
await raceRegistrationsUseCase.execute({ raceId });
|
||||
const registrationsViewModel = raceRegistrationsUseCase.presenter.getViewModel();
|
||||
const registeredDriverIds = registrationsViewModel.registeredDriverIds;
|
||||
|
||||
const drivers = await Promise.all(
|
||||
registeredDriverIds.map((id: string) => driverRepo.findById(id)),
|
||||
@@ -144,12 +147,13 @@ export default function RaceDetailPage() {
|
||||
const validDrivers = drivers.filter((d: Driver | null): d is Driver => d !== null);
|
||||
setEntryList(validDrivers);
|
||||
|
||||
const isRegisteredQuery = getIsDriverRegisteredForRaceQuery();
|
||||
const userIsRegistered = await isRegisteredQuery.execute({
|
||||
const isRegisteredUseCase = getIsDriverRegisteredForRaceUseCase();
|
||||
await isRegisteredUseCase.execute({
|
||||
raceId,
|
||||
driverId: currentDriverId,
|
||||
});
|
||||
setIsUserRegistered(userIsRegistered);
|
||||
const registrationViewModel = isRegisteredUseCase.presenter.getViewModel();
|
||||
setIsUserRegistered(registrationViewModel.isRegistered);
|
||||
|
||||
const membership = getMembership(leagueId, currentDriverId);
|
||||
const isUpcoming = race?.status === 'scheduled';
|
||||
|
||||
@@ -18,8 +18,8 @@ import {
|
||||
getResultRepository,
|
||||
getStandingRepository,
|
||||
getDriverRepository,
|
||||
getGetRaceWithSOFQuery,
|
||||
getGetRacePenaltiesQuery,
|
||||
getGetRaceWithSOFUseCase,
|
||||
getGetRacePenaltiesUseCase,
|
||||
} from '@/lib/di-container';
|
||||
|
||||
interface PenaltyData {
|
||||
@@ -52,7 +52,7 @@ export default function RaceResultsPage() {
|
||||
const leagueRepo = getLeagueRepository();
|
||||
const resultRepo = getResultRepository();
|
||||
const driverRepo = getDriverRepository();
|
||||
const raceWithSOFQuery = getGetRaceWithSOFQuery();
|
||||
const raceWithSOFUseCase = getGetRaceWithSOFUseCase();
|
||||
|
||||
const raceData = await raceRepo.findById(raceId);
|
||||
|
||||
@@ -64,10 +64,11 @@ export default function RaceResultsPage() {
|
||||
|
||||
setRace(raceData);
|
||||
|
||||
// Load race with SOF from application query
|
||||
const raceWithSOF = await raceWithSOFQuery.execute({ raceId });
|
||||
if (raceWithSOF) {
|
||||
setRaceSOF(raceWithSOF.strengthOfField);
|
||||
// Load race with SOF from application use case
|
||||
await raceWithSOFUseCase.execute({ raceId });
|
||||
const raceViewModel = raceWithSOFUseCase.presenter.getViewModel();
|
||||
if (raceViewModel) {
|
||||
setRaceSOF(raceViewModel.strengthOfField);
|
||||
}
|
||||
|
||||
// Load league data
|
||||
@@ -89,10 +90,11 @@ export default function RaceResultsPage() {
|
||||
|
||||
// Load penalties for this race
|
||||
try {
|
||||
const penaltiesQuery = getGetRacePenaltiesQuery();
|
||||
const penaltiesData = await penaltiesQuery.execute(raceId);
|
||||
const penaltiesUseCase = getGetRacePenaltiesUseCase();
|
||||
await penaltiesUseCase.execute(raceId);
|
||||
const penaltiesViewModel = penaltiesUseCase.presenter.getViewModel();
|
||||
// Map the DTO to the PenaltyData interface expected by ResultsTable
|
||||
setPenalties(penaltiesData.map(p => ({
|
||||
setPenalties(penaltiesViewModel.map(p => ({
|
||||
driverId: p.driverId,
|
||||
type: p.type,
|
||||
value: p.value,
|
||||
|
||||
@@ -7,8 +7,7 @@ import Card from '@/components/ui/Card';
|
||||
import Button from '@/components/ui/Button';
|
||||
import Heading from '@/components/ui/Heading';
|
||||
import { Race, RaceStatus } from '@gridpilot/racing/domain/entities/Race';
|
||||
import { League } from '@gridpilot/racing/domain/entities/League';
|
||||
import { getRaceRepository, getLeagueRepository } from '@/lib/di-container';
|
||||
import { getGetRacesPageDataUseCase } from '@/lib/di-container';
|
||||
import {
|
||||
Calendar,
|
||||
Clock,
|
||||
@@ -32,8 +31,13 @@ type TimeFilter = 'all' | 'upcoming' | 'live' | 'past';
|
||||
export default function RacesPage() {
|
||||
const router = useRouter();
|
||||
|
||||
const [races, setRaces] = useState<Race[]>([]);
|
||||
const [leagues, setLeagues] = useState<Map<string, League>>(new Map());
|
||||
const [pageData, setPageData] = useState<{
|
||||
races: Array<{ race: Race; leagueName: string }>;
|
||||
stats: { total: number; scheduled: number; running: number; completed: number };
|
||||
liveRaces: Array<{ race: Race; leagueName: string }>;
|
||||
upcomingRaces: Array<{ race: Race; leagueName: string }>;
|
||||
recentResults: Array<{ race: Race; leagueName: string }>;
|
||||
} | null>(null);
|
||||
const [loading, setLoading] = useState(true);
|
||||
|
||||
// Filters
|
||||
@@ -43,19 +47,74 @@ export default function RacesPage() {
|
||||
|
||||
const loadRaces = async () => {
|
||||
try {
|
||||
const raceRepo = getRaceRepository();
|
||||
const leagueRepo = getLeagueRepository();
|
||||
|
||||
const [allRaces, allLeagues] = await Promise.all([
|
||||
raceRepo.findAll(),
|
||||
leagueRepo.findAll()
|
||||
]);
|
||||
|
||||
setRaces(allRaces.sort((a, b) => a.scheduledAt.getTime() - b.scheduledAt.getTime()));
|
||||
const useCase = getGetRacesPageDataUseCase();
|
||||
await useCase.execute();
|
||||
const data = useCase.presenter.getViewModel();
|
||||
|
||||
const leagueMap = new Map<string, League>();
|
||||
allLeagues.forEach(league => leagueMap.set(league.id, league));
|
||||
setLeagues(leagueMap);
|
||||
// Transform ViewModel back to page format
|
||||
setPageData({
|
||||
races: data.races.map(r => ({
|
||||
race: {
|
||||
id: r.id,
|
||||
track: r.track,
|
||||
car: r.car,
|
||||
scheduledAt: new Date(r.scheduledAt),
|
||||
status: r.status,
|
||||
leagueId: r.leagueId,
|
||||
strengthOfField: r.strengthOfField,
|
||||
isUpcoming: () => r.isUpcoming,
|
||||
isLive: () => r.isLive,
|
||||
isPast: () => r.isPast,
|
||||
} as Race,
|
||||
leagueName: r.leagueName,
|
||||
})),
|
||||
stats: data.stats,
|
||||
liveRaces: data.liveRaces.map(r => ({
|
||||
race: {
|
||||
id: r.id,
|
||||
track: r.track,
|
||||
car: r.car,
|
||||
scheduledAt: new Date(r.scheduledAt),
|
||||
status: r.status,
|
||||
leagueId: r.leagueId,
|
||||
strengthOfField: r.strengthOfField,
|
||||
isUpcoming: () => r.isUpcoming,
|
||||
isLive: () => r.isLive,
|
||||
isPast: () => r.isPast,
|
||||
} as Race,
|
||||
leagueName: r.leagueName,
|
||||
})),
|
||||
upcomingRaces: data.upcomingThisWeek.map(r => ({
|
||||
race: {
|
||||
id: r.id,
|
||||
track: r.track,
|
||||
car: r.car,
|
||||
scheduledAt: new Date(r.scheduledAt),
|
||||
status: r.status,
|
||||
leagueId: r.leagueId,
|
||||
strengthOfField: r.strengthOfField,
|
||||
isUpcoming: () => r.isUpcoming,
|
||||
isLive: () => r.isLive,
|
||||
isPast: () => r.isPast,
|
||||
} as Race,
|
||||
leagueName: r.leagueName,
|
||||
})),
|
||||
recentResults: data.recentResults.map(r => ({
|
||||
race: {
|
||||
id: r.id,
|
||||
track: r.track,
|
||||
car: r.car,
|
||||
scheduledAt: new Date(r.scheduledAt),
|
||||
status: r.status,
|
||||
leagueId: r.leagueId,
|
||||
strengthOfField: r.strengthOfField,
|
||||
isUpcoming: () => r.isUpcoming,
|
||||
isLive: () => r.isLive,
|
||||
isPast: () => r.isPast,
|
||||
} as Race,
|
||||
leagueName: r.leagueName,
|
||||
})),
|
||||
});
|
||||
} catch (err) {
|
||||
console.error('Failed to load races:', err);
|
||||
} finally {
|
||||
@@ -69,7 +128,9 @@ export default function RacesPage() {
|
||||
|
||||
// Filter races
|
||||
const filteredRaces = useMemo(() => {
|
||||
return races.filter(race => {
|
||||
if (!pageData) return [];
|
||||
|
||||
return pageData.races.filter(({ race }) => {
|
||||
// Status filter
|
||||
if (statusFilter !== 'all' && race.status !== statusFilter) {
|
||||
return false;
|
||||
@@ -93,53 +154,25 @@ export default function RacesPage() {
|
||||
|
||||
return true;
|
||||
});
|
||||
}, [races, statusFilter, leagueFilter, timeFilter]);
|
||||
}, [pageData, statusFilter, leagueFilter, timeFilter]);
|
||||
|
||||
// Group races by date for calendar view
|
||||
const racesByDate = useMemo(() => {
|
||||
const grouped = new Map<string, Race[]>();
|
||||
filteredRaces.forEach(race => {
|
||||
const dateKey = race.scheduledAt.toISOString().split('T')[0];
|
||||
const grouped = new Map<string, Array<{ race: Race; leagueName: string }>>();
|
||||
filteredRaces.forEach(item => {
|
||||
const dateKey = item.race.scheduledAt.toISOString().split('T')[0];
|
||||
if (!grouped.has(dateKey)) {
|
||||
grouped.set(dateKey, []);
|
||||
}
|
||||
grouped.get(dateKey)!.push(race);
|
||||
grouped.get(dateKey)!.push(item);
|
||||
});
|
||||
return grouped;
|
||||
}, [filteredRaces]);
|
||||
|
||||
// Get upcoming races (next 7 days)
|
||||
const upcomingRaces = useMemo(() => {
|
||||
const now = new Date();
|
||||
const nextWeek = new Date(now.getTime() + 7 * 24 * 60 * 60 * 1000);
|
||||
return races.filter(race =>
|
||||
race.isUpcoming() &&
|
||||
race.scheduledAt >= now &&
|
||||
race.scheduledAt <= nextWeek
|
||||
).slice(0, 5);
|
||||
}, [races]);
|
||||
|
||||
// Get live races
|
||||
const liveRaces = useMemo(() => {
|
||||
return races.filter(race => race.isLive());
|
||||
}, [races]);
|
||||
|
||||
// Get recent results
|
||||
const recentResults = useMemo(() => {
|
||||
return races
|
||||
.filter(race => race.status === 'completed')
|
||||
.sort((a, b) => b.scheduledAt.getTime() - a.scheduledAt.getTime())
|
||||
.slice(0, 3);
|
||||
}, [races]);
|
||||
|
||||
// Stats
|
||||
const stats = useMemo(() => {
|
||||
const total = races.length;
|
||||
const scheduled = races.filter(r => r.status === 'scheduled').length;
|
||||
const running = races.filter(r => r.status === 'running').length;
|
||||
const completed = races.filter(r => r.status === 'completed').length;
|
||||
return { total, scheduled, running, completed };
|
||||
}, [races]);
|
||||
const upcomingRaces = pageData?.upcomingRaces ?? [];
|
||||
const liveRaces = pageData?.liveRaces ?? [];
|
||||
const recentResults = pageData?.recentResults ?? [];
|
||||
const stats = pageData?.stats ?? { total: 0, scheduled: 0, running: 0, completed: 0 };
|
||||
|
||||
const formatDate = (date: Date) => {
|
||||
return new Date(date).toLocaleDateString('en-US', {
|
||||
@@ -298,8 +331,8 @@ export default function RacesPage() {
|
||||
</div>
|
||||
|
||||
<div className="space-y-3">
|
||||
{liveRaces.map(race => (
|
||||
<div
|
||||
{liveRaces.map(({ race, leagueName }) => (
|
||||
<div
|
||||
key={race.id}
|
||||
onClick={() => router.push(`/races/${race.id}`)}
|
||||
className="flex items-center justify-between p-4 bg-deep-graphite/80 rounded-lg border border-performance-green/20 cursor-pointer hover:border-performance-green/40 transition-all"
|
||||
@@ -310,7 +343,7 @@ export default function RacesPage() {
|
||||
</div>
|
||||
<div>
|
||||
<h3 className="font-semibold text-white">{race.track}</h3>
|
||||
<p className="text-sm text-gray-400">{leagues.get(race.leagueId)?.name}</p>
|
||||
<p className="text-sm text-gray-400">{leagueName}</p>
|
||||
</div>
|
||||
</div>
|
||||
<ChevronRight className="w-5 h-5 text-gray-400" />
|
||||
@@ -352,11 +385,14 @@ export default function RacesPage() {
|
||||
className="px-4 py-2 bg-deep-graphite border border-charcoal-outline rounded-lg text-white text-sm focus:outline-none focus:ring-2 focus:ring-primary-blue"
|
||||
>
|
||||
<option value="all">All Leagues</option>
|
||||
{Array.from(leagues.values()).map(league => (
|
||||
<option key={league.id} value={league.id}>
|
||||
{league.name}
|
||||
</option>
|
||||
))}
|
||||
{pageData && [...new Set(pageData.races.map(r => r.race.leagueId))].map(leagueId => {
|
||||
const item = pageData.races.find(r => r.race.leagueId === leagueId);
|
||||
return item ? (
|
||||
<option key={leagueId} value={leagueId}>
|
||||
{item.leagueName}
|
||||
</option>
|
||||
) : null;
|
||||
})}
|
||||
</select>
|
||||
</div>
|
||||
</Card>
|
||||
@@ -371,7 +407,7 @@ export default function RacesPage() {
|
||||
<div>
|
||||
<p className="text-white font-medium mb-1">No races found</p>
|
||||
<p className="text-sm text-gray-500">
|
||||
{races.length === 0
|
||||
{pageData?.races.length === 0
|
||||
? 'No races have been scheduled yet'
|
||||
: 'Try adjusting your filters'}
|
||||
</p>
|
||||
@@ -397,10 +433,9 @@ export default function RacesPage() {
|
||||
|
||||
{/* Races for this date */}
|
||||
<div className="space-y-2">
|
||||
{dayRaces.map(race => {
|
||||
{dayRaces.map(({ race, leagueName }) => {
|
||||
const config = statusConfig[race.status];
|
||||
const StatusIcon = config.icon;
|
||||
const league = leagues.get(race.leagueId);
|
||||
|
||||
return (
|
||||
<div
|
||||
@@ -456,19 +491,17 @@ export default function RacesPage() {
|
||||
</div>
|
||||
|
||||
{/* League Link */}
|
||||
{league && (
|
||||
<div className="mt-3 pt-3 border-t border-charcoal-outline/50">
|
||||
<Link
|
||||
href={`/leagues/${league.id}`}
|
||||
onClick={(e) => e.stopPropagation()}
|
||||
className="inline-flex items-center gap-2 text-sm text-primary-blue hover:underline"
|
||||
>
|
||||
<Trophy className="w-3.5 h-3.5" />
|
||||
{league.name}
|
||||
<ArrowRight className="w-3 h-3" />
|
||||
</Link>
|
||||
</div>
|
||||
)}
|
||||
<div className="mt-3 pt-3 border-t border-charcoal-outline/50">
|
||||
<Link
|
||||
href={`/leagues/${race.leagueId}`}
|
||||
onClick={(e) => e.stopPropagation()}
|
||||
className="inline-flex items-center gap-2 text-sm text-primary-blue hover:underline"
|
||||
>
|
||||
<Trophy className="w-3.5 h-3.5" />
|
||||
{leagueName}
|
||||
<ArrowRight className="w-3 h-3" />
|
||||
</Link>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Arrow */}
|
||||
@@ -515,8 +548,8 @@ export default function RacesPage() {
|
||||
</p>
|
||||
) : (
|
||||
<div className="space-y-3">
|
||||
{upcomingRaces.map((race, index) => (
|
||||
<div
|
||||
{upcomingRaces.map(({ race }) => (
|
||||
<div
|
||||
key={race.id}
|
||||
onClick={() => router.push(`/races/${race.id}`)}
|
||||
className="flex items-center gap-3 p-2 rounded-lg hover:bg-deep-graphite cursor-pointer transition-colors"
|
||||
@@ -552,8 +585,8 @@ export default function RacesPage() {
|
||||
</p>
|
||||
) : (
|
||||
<div className="space-y-3">
|
||||
{recentResults.map(race => (
|
||||
<div
|
||||
{recentResults.map(({ race }) => (
|
||||
<div
|
||||
key={race.id}
|
||||
onClick={() => router.push(`/races/${race.id}/results`)}
|
||||
className="flex items-center gap-3 p-2 rounded-lg hover:bg-deep-graphite cursor-pointer transition-colors"
|
||||
|
||||
@@ -32,57 +32,73 @@ interface Sponsorship {
|
||||
drivers: number;
|
||||
}
|
||||
|
||||
// Mock data - in production would come from repository
|
||||
const MOCK_SPONSORSHIPS: Sponsorship[] = [
|
||||
{
|
||||
id: 'sp-1',
|
||||
leagueId: 'league-1',
|
||||
leagueName: 'GT3 Pro Championship',
|
||||
tier: 'main',
|
||||
status: 'active',
|
||||
startDate: new Date('2025-01-01'),
|
||||
endDate: new Date('2025-06-30'),
|
||||
price: 1200,
|
||||
impressions: 45200,
|
||||
drivers: 32,
|
||||
},
|
||||
{
|
||||
id: 'sp-2',
|
||||
leagueId: 'league-2',
|
||||
leagueName: 'Endurance Masters',
|
||||
tier: 'main',
|
||||
status: 'active',
|
||||
startDate: new Date('2025-02-01'),
|
||||
endDate: new Date('2025-07-31'),
|
||||
price: 1000,
|
||||
impressions: 38100,
|
||||
drivers: 48,
|
||||
},
|
||||
{
|
||||
id: 'sp-3',
|
||||
leagueId: 'league-3',
|
||||
leagueName: 'Formula Sim Series',
|
||||
tier: 'secondary',
|
||||
status: 'active',
|
||||
startDate: new Date('2025-03-01'),
|
||||
endDate: new Date('2025-08-31'),
|
||||
price: 400,
|
||||
impressions: 22800,
|
||||
drivers: 24,
|
||||
},
|
||||
{
|
||||
id: 'sp-4',
|
||||
leagueId: 'league-4',
|
||||
leagueName: 'Touring Car Cup',
|
||||
tier: 'secondary',
|
||||
status: 'pending',
|
||||
startDate: new Date('2025-04-01'),
|
||||
endDate: new Date('2025-09-30'),
|
||||
price: 350,
|
||||
impressions: 0,
|
||||
drivers: 28,
|
||||
},
|
||||
];
|
||||
interface SponsorshipDetailApi {
|
||||
id: string;
|
||||
leagueId: string;
|
||||
leagueName: string;
|
||||
seasonId: string;
|
||||
seasonName: string;
|
||||
seasonStartDate?: string;
|
||||
seasonEndDate?: string;
|
||||
tier: 'main' | 'secondary';
|
||||
status: string;
|
||||
pricing: {
|
||||
amount: number;
|
||||
currency: string;
|
||||
};
|
||||
metrics: {
|
||||
drivers: number;
|
||||
races: number;
|
||||
completedRaces: number;
|
||||
impressions: number;
|
||||
};
|
||||
createdAt: string;
|
||||
activatedAt?: string;
|
||||
}
|
||||
|
||||
interface SponsorSponsorshipsResponse {
|
||||
sponsorId: string;
|
||||
sponsorName: string;
|
||||
sponsorships: SponsorshipDetailApi[];
|
||||
summary: {
|
||||
totalSponsorships: number;
|
||||
activeSponsorships: number;
|
||||
totalInvestment: number;
|
||||
totalPlatformFees: number;
|
||||
currency: string;
|
||||
};
|
||||
}
|
||||
|
||||
function mapSponsorshipStatus(status: string): 'active' | 'pending' | 'expired' {
|
||||
switch (status) {
|
||||
case 'active':
|
||||
return 'active';
|
||||
case 'pending':
|
||||
return 'pending';
|
||||
default:
|
||||
return 'expired';
|
||||
}
|
||||
}
|
||||
|
||||
function mapApiToSponsorships(response: SponsorSponsorshipsResponse): Sponsorship[] {
|
||||
return response.sponsorships.map((s) => {
|
||||
const start = s.seasonStartDate ? new Date(s.seasonStartDate) : new Date(s.createdAt);
|
||||
const end = s.seasonEndDate ? new Date(s.seasonEndDate) : start;
|
||||
|
||||
return {
|
||||
id: s.id,
|
||||
leagueId: s.leagueId,
|
||||
leagueName: s.leagueName,
|
||||
tier: s.tier,
|
||||
status: mapSponsorshipStatus(s.status),
|
||||
startDate: start,
|
||||
endDate: end,
|
||||
price: s.pricing.amount,
|
||||
impressions: s.metrics.impressions,
|
||||
drivers: s.metrics.drivers,
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
function SponsorshipCard({ sponsorship }: { sponsorship: Sponsorship }) {
|
||||
const router = useRouter();
|
||||
@@ -179,18 +195,59 @@ function SponsorshipCard({ sponsorship }: { sponsorship: Sponsorship }) {
|
||||
export default function SponsorCampaignsPage() {
|
||||
const router = useRouter();
|
||||
const [filter, setFilter] = useState<'all' | 'active' | 'pending' | 'expired'>('all');
|
||||
|
||||
const filteredSponsorships = filter === 'all'
|
||||
? MOCK_SPONSORSHIPS
|
||||
: MOCK_SPONSORSHIPS.filter(s => s.status === filter);
|
||||
const [sponsorships, setSponsorships] = useState<Sponsorship[]>([]);
|
||||
const [loading, setLoading] = useState(true);
|
||||
|
||||
useEffect(() => {
|
||||
let isMounted = true;
|
||||
|
||||
async function fetchSponsorships() {
|
||||
try {
|
||||
const response = await fetch('/api/sponsors/sponsorships');
|
||||
if (!response.ok) {
|
||||
if (!isMounted) return;
|
||||
setSponsorships([]);
|
||||
return;
|
||||
}
|
||||
const json: SponsorSponsorshipsResponse = await response.json();
|
||||
if (!isMounted) return;
|
||||
setSponsorships(mapApiToSponsorships(json));
|
||||
} catch {
|
||||
if (!isMounted) return;
|
||||
setSponsorships([]);
|
||||
} finally {
|
||||
if (isMounted) {
|
||||
setLoading(false);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fetchSponsorships();
|
||||
|
||||
return () => {
|
||||
isMounted = false;
|
||||
};
|
||||
}, []);
|
||||
|
||||
const filteredSponsorships = filter === 'all'
|
||||
? sponsorships
|
||||
: sponsorships.filter(s => s.status === filter);
|
||||
|
||||
const stats = {
|
||||
total: MOCK_SPONSORSHIPS.length,
|
||||
active: MOCK_SPONSORSHIPS.filter(s => s.status === 'active').length,
|
||||
pending: MOCK_SPONSORSHIPS.filter(s => s.status === 'pending').length,
|
||||
totalInvestment: MOCK_SPONSORSHIPS.reduce((sum, s) => sum + s.price, 0),
|
||||
total: sponsorships.length,
|
||||
active: sponsorships.filter(s => s.status === 'active').length,
|
||||
pending: sponsorships.filter(s => s.status === 'pending').length,
|
||||
totalInvestment: sponsorships.reduce((sum, s) => sum + s.price, 0),
|
||||
};
|
||||
|
||||
if (loading) {
|
||||
return (
|
||||
<div className="max-w-6xl mx-auto py-8 px-4">
|
||||
<p className="text-gray-400">Loading sponsorships…</p>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="max-w-6xl mx-auto py-8 px-4">
|
||||
{/* Header */}
|
||||
|
||||
@@ -52,64 +52,6 @@ interface SponsorDashboardData {
|
||||
};
|
||||
}
|
||||
|
||||
// Fallback mock data for demo mode
|
||||
const MOCK_DASHBOARD: SponsorDashboardData = {
|
||||
sponsorId: 'demo-sponsor',
|
||||
sponsorName: 'Demo Sponsor',
|
||||
metrics: {
|
||||
impressions: 124500,
|
||||
impressionsChange: 12.5,
|
||||
uniqueViewers: 8420,
|
||||
viewersChange: 8.3,
|
||||
races: 24,
|
||||
drivers: 156,
|
||||
exposure: 87.5,
|
||||
exposureChange: 5.2,
|
||||
},
|
||||
sponsoredLeagues: [
|
||||
{
|
||||
id: 'league-1',
|
||||
name: 'GT3 Pro Championship',
|
||||
tier: 'main',
|
||||
drivers: 32,
|
||||
races: 12,
|
||||
impressions: 45200,
|
||||
status: 'active',
|
||||
},
|
||||
{
|
||||
id: 'league-2',
|
||||
name: 'Endurance Masters',
|
||||
tier: 'main',
|
||||
drivers: 48,
|
||||
races: 6,
|
||||
impressions: 38100,
|
||||
status: 'active',
|
||||
},
|
||||
{
|
||||
id: 'league-3',
|
||||
name: 'Formula Sim Series',
|
||||
tier: 'secondary',
|
||||
drivers: 24,
|
||||
races: 8,
|
||||
impressions: 22800,
|
||||
status: 'active',
|
||||
},
|
||||
{
|
||||
id: 'league-4',
|
||||
name: 'Touring Car Cup',
|
||||
tier: 'secondary',
|
||||
drivers: 28,
|
||||
races: 10,
|
||||
impressions: 18400,
|
||||
status: 'upcoming',
|
||||
},
|
||||
],
|
||||
investment: {
|
||||
activeSponsorships: 4,
|
||||
totalInvestment: 2400,
|
||||
costPerThousandViews: 19.28,
|
||||
},
|
||||
};
|
||||
|
||||
function MetricCard({
|
||||
title,
|
||||
@@ -205,15 +147,13 @@ export default function SponsorDashboardPage() {
|
||||
try {
|
||||
const response = await fetch('/api/sponsors/dashboard');
|
||||
if (response.ok) {
|
||||
const dashboardData = await response.json();
|
||||
const dashboardData: SponsorDashboardData = await response.json();
|
||||
setData(dashboardData);
|
||||
} else {
|
||||
// Use mock data for demo mode
|
||||
setData(MOCK_DASHBOARD);
|
||||
setError('Failed to load sponsor dashboard');
|
||||
}
|
||||
} catch {
|
||||
// Use mock data on error
|
||||
setData(MOCK_DASHBOARD);
|
||||
setError('Failed to load sponsor dashboard');
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
@@ -230,7 +170,19 @@ export default function SponsorDashboardPage() {
|
||||
);
|
||||
}
|
||||
|
||||
const dashboardData = data || MOCK_DASHBOARD;
|
||||
if (!data) {
|
||||
return (
|
||||
<div className="max-w-7xl mx-auto py-8 px-4">
|
||||
<div className="rounded-lg bg-warning-amber/10 border border-warning-amber/30 p-4">
|
||||
<p className="text-sm text-warning-amber">
|
||||
{error ?? 'No sponsor dashboard data available yet.'}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
const dashboardData = data;
|
||||
|
||||
return (
|
||||
<div className="max-w-7xl mx-auto py-8 px-4">
|
||||
|
||||
@@ -14,8 +14,8 @@ import TeamStandings from '@/components/teams/TeamStandings';
|
||||
import TeamAdmin from '@/components/teams/TeamAdmin';
|
||||
import JoinTeamButton from '@/components/teams/JoinTeamButton';
|
||||
import {
|
||||
getGetTeamDetailsQuery,
|
||||
getGetTeamMembersQuery,
|
||||
getGetTeamDetailsUseCase,
|
||||
getGetTeamMembersUseCase,
|
||||
getTeamMembershipRepository,
|
||||
} from '@/lib/di-container';
|
||||
import { useEffectiveDriverId } from '@/lib/currentDriver';
|
||||
@@ -39,11 +39,15 @@ export default function TeamDetailPage() {
|
||||
const loadTeamData = useCallback(async () => {
|
||||
setLoading(true);
|
||||
try {
|
||||
const detailsQuery = getGetTeamDetailsQuery();
|
||||
const membersQuery = getGetTeamMembersQuery();
|
||||
const detailsUseCase = getGetTeamDetailsUseCase();
|
||||
const membersUseCase = getGetTeamMembersUseCase();
|
||||
|
||||
const details = await detailsQuery.execute({ teamId, driverId: currentDriverId });
|
||||
const teamMemberships = await membersQuery.execute({ teamId });
|
||||
await detailsUseCase.execute({ teamId, driverId: currentDriverId });
|
||||
const detailsViewModel = detailsUseCase.presenter.getViewModel();
|
||||
|
||||
await membersUseCase.execute({ teamId });
|
||||
const membersViewModel = membersUseCase.presenter.getViewModel();
|
||||
const teamMemberships = membersViewModel.members;
|
||||
|
||||
const adminStatus =
|
||||
teamMemberships.some(
|
||||
@@ -52,7 +56,7 @@ export default function TeamDetailPage() {
|
||||
(m.role === 'owner' || m.role === 'manager'),
|
||||
) ?? false;
|
||||
|
||||
setTeam(details.team);
|
||||
setTeam(detailsViewModel.team);
|
||||
setMemberships(teamMemberships);
|
||||
setIsAdmin(adminStatus);
|
||||
} finally {
|
||||
|
||||
@@ -22,7 +22,7 @@ import {
|
||||
import Button from '@/components/ui/Button';
|
||||
import Input from '@/components/ui/Input';
|
||||
import Heading from '@/components/ui/Heading';
|
||||
import { getGetAllTeamsQuery, getGetTeamMembersQuery, getDriverStats } from '@/lib/di-container';
|
||||
import { getGetAllTeamsUseCase, getGetTeamMembersUseCase, getDriverStats } from '@/lib/di-container';
|
||||
import type { Team } from '@gridpilot/racing';
|
||||
|
||||
// ============================================================================
|
||||
@@ -260,15 +260,19 @@ export default function TeamLeaderboardPage() {
|
||||
|
||||
const loadTeams = async () => {
|
||||
try {
|
||||
const allTeamsQuery = getGetAllTeamsQuery();
|
||||
const teamMembersQuery = getGetTeamMembersQuery();
|
||||
const allTeamsUseCase = getGetAllTeamsUseCase();
|
||||
const teamMembersUseCase = getGetTeamMembersUseCase();
|
||||
|
||||
const allTeams = await allTeamsQuery.execute();
|
||||
await allTeamsUseCase.execute();
|
||||
const allTeamsViewModel = allTeamsUseCase.presenter.getViewModel();
|
||||
const allTeams = allTeamsViewModel.teams;
|
||||
const teamData: TeamDisplayData[] = [];
|
||||
|
||||
await Promise.all(
|
||||
allTeams.map(async (team: Team) => {
|
||||
const memberships = await teamMembersQuery.execute({ teamId: team.id });
|
||||
await teamMembersUseCase.execute({ teamId: team.id });
|
||||
const membershipsViewModel = teamMembersUseCase.presenter.getViewModel();
|
||||
const memberships = membershipsViewModel.members;
|
||||
const memberCount = memberships.length;
|
||||
|
||||
let ratingSum = 0;
|
||||
|
||||
@@ -28,30 +28,14 @@ import Card from '@/components/ui/Card';
|
||||
import Input from '@/components/ui/Input';
|
||||
import Heading from '@/components/ui/Heading';
|
||||
import CreateTeamForm from '@/components/teams/CreateTeamForm';
|
||||
import { getGetAllTeamsQuery, getGetTeamMembersQuery, getDriverStats } from '@/lib/di-container';
|
||||
import type { Team } from '@gridpilot/racing';
|
||||
import { getGetTeamsLeaderboardUseCase } from '@/lib/di-container';
|
||||
import type { TeamLeaderboardItemViewModel, SkillLevel } from '@gridpilot/racing/application/presenters/ITeamsLeaderboardPresenter';
|
||||
|
||||
// ============================================================================
|
||||
// TYPES
|
||||
// ============================================================================
|
||||
|
||||
type SkillLevel = 'beginner' | 'intermediate' | 'advanced' | 'pro';
|
||||
|
||||
interface TeamDisplayData {
|
||||
id: string;
|
||||
name: string;
|
||||
memberCount: number;
|
||||
rating: number | null;
|
||||
totalWins: number;
|
||||
totalRaces: number;
|
||||
performanceLevel: SkillLevel;
|
||||
isRecruiting: boolean;
|
||||
createdAt: Date;
|
||||
description?: string;
|
||||
specialization?: 'endurance' | 'sprint' | 'mixed';
|
||||
region?: string;
|
||||
languages?: string[];
|
||||
}
|
||||
type TeamDisplayData = TeamLeaderboardItemViewModel;
|
||||
|
||||
// ============================================================================
|
||||
// SKILL LEVEL CONFIG
|
||||
@@ -463,59 +447,10 @@ export default function TeamsPage() {
|
||||
|
||||
const loadTeams = async () => {
|
||||
try {
|
||||
const allTeamsQuery = getGetAllTeamsQuery();
|
||||
const teamMembersQuery = getGetTeamMembersQuery();
|
||||
|
||||
const allTeams = await allTeamsQuery.execute();
|
||||
const teamData: TeamDisplayData[] = [];
|
||||
|
||||
await Promise.all(
|
||||
allTeams.map(async (team: Team) => {
|
||||
const memberships = await teamMembersQuery.execute({ teamId: team.id });
|
||||
const memberCount = memberships.length;
|
||||
|
||||
let ratingSum = 0;
|
||||
let ratingCount = 0;
|
||||
let totalWins = 0;
|
||||
let totalRaces = 0;
|
||||
|
||||
for (const membership of memberships) {
|
||||
const stats = getDriverStats(membership.driverId);
|
||||
if (!stats) continue;
|
||||
|
||||
if (typeof stats.rating === 'number') {
|
||||
ratingSum += stats.rating;
|
||||
ratingCount += 1;
|
||||
}
|
||||
|
||||
totalWins += stats.wins ?? 0;
|
||||
totalRaces += stats.totalRaces ?? 0;
|
||||
}
|
||||
|
||||
const averageRating = ratingCount > 0 ? ratingSum / ratingCount : null;
|
||||
|
||||
let performanceLevel: TeamDisplayData['performanceLevel'] = 'beginner';
|
||||
if (averageRating !== null) {
|
||||
if (averageRating >= 4500) performanceLevel = 'pro';
|
||||
else if (averageRating >= 3000) performanceLevel = 'advanced';
|
||||
else if (averageRating >= 2000) performanceLevel = 'intermediate';
|
||||
}
|
||||
|
||||
teamData.push({
|
||||
id: team.id,
|
||||
name: team.name,
|
||||
memberCount,
|
||||
rating: averageRating,
|
||||
totalWins,
|
||||
totalRaces,
|
||||
performanceLevel,
|
||||
isRecruiting: true,
|
||||
createdAt: new Date(),
|
||||
});
|
||||
}),
|
||||
);
|
||||
|
||||
setRealTeams(teamData);
|
||||
const useCase = getGetTeamsLeaderboardUseCase();
|
||||
await useCase.execute();
|
||||
const viewModel = useCase.presenter.getViewModel();
|
||||
setRealTeams(viewModel.teams);
|
||||
} catch (error) {
|
||||
console.error('Failed to load teams:', error);
|
||||
} finally {
|
||||
|
||||
@@ -8,7 +8,7 @@ import CareerHighlights from './CareerHighlights';
|
||||
import DriverRankings from './DriverRankings';
|
||||
import PerformanceMetrics from './PerformanceMetrics';
|
||||
import { useEffect, useState } from 'react';
|
||||
import { getDriverStats, getLeagueRankings, getGetDriverTeamQuery, getAllDriverRankings } from '@/lib/di-container';
|
||||
import { getDriverStats, getLeagueRankings, getGetDriverTeamUseCase, getAllDriverRankings } from '@/lib/di-container';
|
||||
import { getPrimaryLeagueIdForDriver } from '@/lib/leagueMembership';
|
||||
import type { GetDriverTeamQueryResultDTO } from '@gridpilot/racing/application/dto/TeamCommandAndQueryDTO';
|
||||
|
||||
@@ -29,9 +29,10 @@ export default function DriverProfile({ driver, isOwnProfile = false, onEditClic
|
||||
|
||||
useEffect(() => {
|
||||
const load = async () => {
|
||||
const query = getGetDriverTeamQuery();
|
||||
const result = await query.execute({ driverId: driver.id });
|
||||
setTeamData(result);
|
||||
const useCase = getGetDriverTeamUseCase();
|
||||
await useCase.execute({ driverId: driver.id });
|
||||
const viewModel = useCase.presenter.getViewModel();
|
||||
setTeamData(viewModel.result);
|
||||
};
|
||||
void load();
|
||||
}, [driver.id]);
|
||||
|
||||
@@ -6,7 +6,7 @@ import Input from '../ui/Input';
|
||||
import { DollarSign, Star, Award, Plus, X, Bell } from 'lucide-react';
|
||||
import PendingSponsorshipRequests, { type PendingRequestDTO } from '../sponsors/PendingSponsorshipRequests';
|
||||
import {
|
||||
getGetPendingSponsorshipRequestsQuery,
|
||||
getGetPendingSponsorshipRequestsUseCase,
|
||||
getAcceptSponsorshipRequestUseCase,
|
||||
getRejectSponsorshipRequestUseCase,
|
||||
getSeasonRepository,
|
||||
@@ -71,8 +71,8 @@ export function LeagueSponsorshipsSection({
|
||||
|
||||
setRequestsLoading(true);
|
||||
try {
|
||||
const query = getGetPendingSponsorshipRequestsQuery();
|
||||
const result = await query.execute({
|
||||
const useCase = getGetPendingSponsorshipRequestsUseCase();
|
||||
await useCase.execute({
|
||||
entityType: 'season',
|
||||
entityId: seasonId,
|
||||
});
|
||||
|
||||
@@ -5,7 +5,7 @@ import Button from '@/components/ui/Button';
|
||||
import {
|
||||
getJoinTeamUseCase,
|
||||
getLeaveTeamUseCase,
|
||||
getGetDriverTeamQuery,
|
||||
getGetDriverTeamUseCase,
|
||||
getTeamMembershipRepository,
|
||||
} from '@/lib/di-container';
|
||||
import { useEffectiveDriverId } from '@/lib/currentDriver';
|
||||
@@ -34,11 +34,12 @@ export default function JoinTeamButton({
|
||||
const m = await membershipRepo.getMembership(teamId, currentDriverId);
|
||||
setMembership(m);
|
||||
|
||||
const driverTeamQuery = getGetDriverTeamQuery();
|
||||
const driverTeam = await driverTeamQuery.execute({ driverId: currentDriverId });
|
||||
if (driverTeam) {
|
||||
setCurrentTeamId(driverTeam.team.id);
|
||||
setCurrentTeamName(driverTeam.team.name);
|
||||
const driverTeamUseCase = getGetDriverTeamUseCase();
|
||||
await driverTeamUseCase.execute({ driverId: currentDriverId });
|
||||
const viewModel = driverTeamUseCase.presenter.getViewModel();
|
||||
if (viewModel.result) {
|
||||
setCurrentTeamId(viewModel.result.team.id);
|
||||
setCurrentTeamName(viewModel.result.team.name);
|
||||
} else {
|
||||
setCurrentTeamId(null);
|
||||
setCurrentTeamName(null);
|
||||
|
||||
@@ -6,7 +6,7 @@ import Button from '@/components/ui/Button';
|
||||
import Input from '@/components/ui/Input';
|
||||
import {
|
||||
getDriverRepository,
|
||||
getGetTeamJoinRequestsQuery,
|
||||
getGetTeamJoinRequestsUseCase,
|
||||
getApproveTeamJoinRequestUseCase,
|
||||
getRejectTeamJoinRequestUseCase,
|
||||
getUpdateTeamUseCase,
|
||||
@@ -36,15 +36,16 @@ export default function TeamAdmin({ team, onUpdate }: TeamAdminProps) {
|
||||
}, [team.id]);
|
||||
|
||||
const loadJoinRequests = async () => {
|
||||
const query = getGetTeamJoinRequestsQuery();
|
||||
const requests = await query.execute({ teamId: team.id });
|
||||
setJoinRequests(requests);
|
||||
const useCase = getGetTeamJoinRequestsUseCase();
|
||||
await useCase.execute({ teamId: team.id });
|
||||
const viewModel = useCase.presenter.getViewModel();
|
||||
setJoinRequests(viewModel.requests);
|
||||
|
||||
const driverRepo = getDriverRepository();
|
||||
const allDrivers = await driverRepo.findAll();
|
||||
const driverMap: Record<string, DriverDTO> = {};
|
||||
|
||||
for (const request of requests) {
|
||||
for (const request of viewModel.requests) {
|
||||
const driver = allDrivers.find(d => d.id === request.driverId);
|
||||
if (driver) {
|
||||
const dto = EntityMappers.toDriverDTO(driver);
|
||||
|
||||
@@ -88,48 +88,77 @@ import {
|
||||
JoinLeagueUseCase,
|
||||
RegisterForRaceUseCase,
|
||||
WithdrawFromRaceUseCase,
|
||||
IsDriverRegisteredForRaceQuery,
|
||||
GetRaceRegistrationsQuery,
|
||||
CreateTeamUseCase,
|
||||
JoinTeamUseCase,
|
||||
LeaveTeamUseCase,
|
||||
ApproveTeamJoinRequestUseCase,
|
||||
RejectTeamJoinRequestUseCase,
|
||||
UpdateTeamUseCase,
|
||||
GetAllTeamsQuery,
|
||||
GetTeamDetailsQuery,
|
||||
GetTeamMembersQuery,
|
||||
GetTeamJoinRequestsQuery,
|
||||
GetDriverTeamQuery,
|
||||
GetLeagueStandingsQuery,
|
||||
GetLeagueDriverSeasonStatsQuery,
|
||||
GetAllLeaguesWithCapacityQuery,
|
||||
GetAllLeaguesWithCapacityAndScoringQuery,
|
||||
ListLeagueScoringPresetsQuery,
|
||||
GetLeagueScoringConfigQuery,
|
||||
GetAllTeamsUseCase,
|
||||
GetTeamDetailsUseCase,
|
||||
GetTeamMembersUseCase,
|
||||
GetTeamJoinRequestsUseCase,
|
||||
GetDriverTeamUseCase,
|
||||
CreateLeagueWithSeasonAndScoringUseCase,
|
||||
GetLeagueFullConfigQuery,
|
||||
GetRaceWithSOFQuery,
|
||||
GetLeagueStatsQuery,
|
||||
FileProtestUseCase,
|
||||
ReviewProtestUseCase,
|
||||
ApplyPenaltyUseCase,
|
||||
GetRaceProtestsQuery,
|
||||
GetRacePenaltiesQuery,
|
||||
RequestProtestDefenseUseCase,
|
||||
SubmitProtestDefenseUseCase,
|
||||
GetSponsorDashboardQuery,
|
||||
GetSponsorSponsorshipsQuery,
|
||||
GetPendingSponsorshipRequestsQuery,
|
||||
GetEntitySponsorshipPricingQuery,
|
||||
GetSponsorDashboardUseCase,
|
||||
GetSponsorSponsorshipsUseCase,
|
||||
GetPendingSponsorshipRequestsUseCase,
|
||||
GetEntitySponsorshipPricingUseCase,
|
||||
ApplyForSponsorshipUseCase,
|
||||
AcceptSponsorshipRequestUseCase,
|
||||
RejectSponsorshipRequestUseCase,
|
||||
} from '@gridpilot/racing/application';
|
||||
import { IsDriverRegisteredForRaceUseCase } from '@gridpilot/racing/application/use-cases/IsDriverRegisteredForRaceQuery';
|
||||
import { GetRaceRegistrationsUseCase } from '@gridpilot/racing/application/use-cases/GetRaceRegistrationsQuery';
|
||||
import { GetRaceWithSOFUseCase } from '@gridpilot/racing/application/use-cases/GetRaceWithSOFQuery';
|
||||
import { GetRaceProtestsUseCase } from '@gridpilot/racing/application/use-cases/GetRaceProtestsQuery';
|
||||
import { GetRacePenaltiesUseCase } from '@gridpilot/racing/application/use-cases/GetRacePenaltiesQuery';
|
||||
import { GetLeagueStandingsUseCase } from '@gridpilot/racing/application/use-cases/GetLeagueStandingsQuery';
|
||||
import { GetLeagueDriverSeasonStatsUseCase } from '@gridpilot/racing/application/use-cases/GetLeagueDriverSeasonStatsQuery';
|
||||
import { GetAllLeaguesWithCapacityUseCase } from '@gridpilot/racing/application/use-cases/GetAllLeaguesWithCapacityQuery';
|
||||
import { GetAllLeaguesWithCapacityAndScoringUseCase } from '@gridpilot/racing/application/use-cases/GetAllLeaguesWithCapacityAndScoringQuery';
|
||||
import { ListLeagueScoringPresetsUseCase } from '@gridpilot/racing/application/use-cases/ListLeagueScoringPresetsQuery';
|
||||
import { GetLeagueScoringConfigUseCase } from '@gridpilot/racing/application/use-cases/GetLeagueScoringConfigQuery';
|
||||
import { GetLeagueFullConfigUseCase } from '@gridpilot/racing/application/use-cases/GetLeagueFullConfigQuery';
|
||||
import { GetLeagueStatsUseCase } from '@gridpilot/racing/application/use-cases/GetLeagueStatsQuery';
|
||||
import { GetRacesPageDataUseCase } from '@gridpilot/racing/application/use-cases/GetRacesPageDataUseCase';
|
||||
import { GetDriversLeaderboardUseCase } from '@gridpilot/racing/application/use-cases/GetDriversLeaderboardUseCase';
|
||||
import { GetTeamsLeaderboardUseCase } from '@gridpilot/racing/application/use-cases/GetTeamsLeaderboardUseCase';
|
||||
import { TransferLeagueOwnershipUseCase } from '@gridpilot/racing/application/use-cases/TransferLeagueOwnershipUseCase';
|
||||
import { DriversLeaderboardPresenter } from '../../lib/presenters/DriversLeaderboardPresenter';
|
||||
import { TeamsLeaderboardPresenter } from '../../lib/presenters/TeamsLeaderboardPresenter';
|
||||
import { RacesPagePresenter } from '../../lib/presenters/RacesPagePresenter';
|
||||
import { AllTeamsPresenter } from '../../lib/presenters/AllTeamsPresenter';
|
||||
import { TeamDetailsPresenter } from '../../lib/presenters/TeamDetailsPresenter';
|
||||
import { TeamMembersPresenter } from '../../lib/presenters/TeamMembersPresenter';
|
||||
import { TeamJoinRequestsPresenter } from '../../lib/presenters/TeamJoinRequestsPresenter';
|
||||
import { DriverTeamPresenter } from '../../lib/presenters/DriverTeamPresenter';
|
||||
import { AllLeaguesWithCapacityPresenter } from '../../lib/presenters/AllLeaguesWithCapacityPresenter';
|
||||
import { AllLeaguesWithCapacityAndScoringPresenter } from '../../lib/presenters/AllLeaguesWithCapacityAndScoringPresenter';
|
||||
import { LeagueStatsPresenter } from '../../lib/presenters/LeagueStatsPresenter';
|
||||
import { LeagueScoringConfigPresenter } from '../../lib/presenters/LeagueScoringConfigPresenter';
|
||||
import { LeagueFullConfigPresenter } from '../../lib/presenters/LeagueFullConfigPresenter';
|
||||
import { LeagueDriverSeasonStatsPresenter } from '../../lib/presenters/LeagueDriverSeasonStatsPresenter';
|
||||
import { LeagueStandingsPresenter } from '../../lib/presenters/LeagueStandingsPresenter';
|
||||
import { LeagueScoringPresetsPresenter } from '../../lib/presenters/LeagueScoringPresetsPresenter';
|
||||
import { RaceWithSOFPresenter } from '../../lib/presenters/RaceWithSOFPresenter';
|
||||
import { RaceProtestsPresenter } from '../../lib/presenters/RaceProtestsPresenter';
|
||||
import { RacePenaltiesPresenter } from '../../lib/presenters/RacePenaltiesPresenter';
|
||||
import { RaceRegistrationsPresenter } from '../../lib/presenters/RaceRegistrationsPresenter';
|
||||
import { DriverRegistrationStatusPresenter } from '../../lib/presenters/DriverRegistrationStatusPresenter';
|
||||
import type { DriverRatingProvider } from '@gridpilot/racing/application';
|
||||
import type { LeagueScoringPresetProvider } from '@gridpilot/racing/application/ports/LeagueScoringPresetProvider';
|
||||
import { PreviewLeagueScheduleQuery } from '@gridpilot/racing/application';
|
||||
import { PreviewLeagueScheduleUseCase } from '@gridpilot/racing/application';
|
||||
import { SponsorDashboardPresenter } from '../../lib/presenters/SponsorDashboardPresenter';
|
||||
import { SponsorSponsorshipsPresenter } from '../../lib/presenters/SponsorSponsorshipsPresenter';
|
||||
import { PendingSponsorshipRequestsPresenter } from '../../lib/presenters/PendingSponsorshipRequestsPresenter';
|
||||
import { EntitySponsorshipPricingPresenter } from '../../lib/presenters/EntitySponsorshipPricingPresenter';
|
||||
import { LeagueSchedulePreviewPresenter } from '../../lib/presenters/LeagueSchedulePreviewPresenter';
|
||||
|
||||
// Testing support
|
||||
import {
|
||||
@@ -840,24 +869,28 @@ export function configureDIContainer(): void {
|
||||
);
|
||||
|
||||
// Register queries - Racing
|
||||
const driverRegistrationStatusPresenter = new DriverRegistrationStatusPresenter();
|
||||
container.registerInstance(
|
||||
DI_TOKENS.IsDriverRegisteredForRaceQuery,
|
||||
new IsDriverRegisteredForRaceQuery(raceRegistrationRepository)
|
||||
DI_TOKENS.IsDriverRegisteredForRaceUseCase,
|
||||
new IsDriverRegisteredForRaceUseCase(raceRegistrationRepository, driverRegistrationStatusPresenter)
|
||||
);
|
||||
|
||||
const raceRegistrationsPresenter = new RaceRegistrationsPresenter();
|
||||
container.registerInstance(
|
||||
DI_TOKENS.GetRaceRegistrationsQuery,
|
||||
new GetRaceRegistrationsQuery(raceRegistrationRepository)
|
||||
DI_TOKENS.GetRaceRegistrationsUseCase,
|
||||
new GetRaceRegistrationsUseCase(raceRegistrationRepository, raceRegistrationsPresenter)
|
||||
);
|
||||
|
||||
const leagueStandingsPresenter = new LeagueStandingsPresenter();
|
||||
container.registerInstance(
|
||||
DI_TOKENS.GetLeagueStandingsQuery,
|
||||
new GetLeagueStandingsQuery(standingRepository)
|
||||
DI_TOKENS.GetLeagueStandingsUseCase,
|
||||
new GetLeagueStandingsUseCase(standingRepository, leagueStandingsPresenter)
|
||||
);
|
||||
|
||||
const leagueDriverSeasonStatsPresenter = new LeagueDriverSeasonStatsPresenter();
|
||||
container.registerInstance(
|
||||
DI_TOKENS.GetLeagueDriverSeasonStatsQuery,
|
||||
new GetLeagueDriverSeasonStatsQuery(
|
||||
DI_TOKENS.GetLeagueDriverSeasonStatsUseCase,
|
||||
new GetLeagueDriverSeasonStatsUseCase(
|
||||
standingRepository,
|
||||
resultRepository,
|
||||
penaltyRepository,
|
||||
@@ -875,113 +908,200 @@ export function configureDIContainer(): void {
|
||||
ratingChange: delta !== 0 ? delta : null,
|
||||
};
|
||||
},
|
||||
}
|
||||
},
|
||||
leagueDriverSeasonStatsPresenter
|
||||
)
|
||||
);
|
||||
|
||||
const allLeaguesWithCapacityPresenter = new AllLeaguesWithCapacityPresenter();
|
||||
container.registerInstance(
|
||||
DI_TOKENS.GetAllLeaguesWithCapacityQuery,
|
||||
new GetAllLeaguesWithCapacityQuery(leagueRepository, leagueMembershipRepository)
|
||||
DI_TOKENS.GetAllLeaguesWithCapacityUseCase,
|
||||
new GetAllLeaguesWithCapacityUseCase(
|
||||
leagueRepository,
|
||||
leagueMembershipRepository,
|
||||
allLeaguesWithCapacityPresenter
|
||||
)
|
||||
);
|
||||
|
||||
const allLeaguesWithCapacityAndScoringPresenter = new AllLeaguesWithCapacityAndScoringPresenter();
|
||||
container.registerInstance(
|
||||
DI_TOKENS.GetAllLeaguesWithCapacityAndScoringQuery,
|
||||
new GetAllLeaguesWithCapacityAndScoringQuery(
|
||||
DI_TOKENS.GetAllLeaguesWithCapacityAndScoringUseCase,
|
||||
new GetAllLeaguesWithCapacityAndScoringUseCase(
|
||||
leagueRepository,
|
||||
leagueMembershipRepository,
|
||||
seasonRepository,
|
||||
leagueScoringConfigRepository,
|
||||
gameRepository,
|
||||
leagueScoringPresetProvider
|
||||
leagueScoringPresetProvider,
|
||||
allLeaguesWithCapacityAndScoringPresenter
|
||||
)
|
||||
);
|
||||
|
||||
const leagueScoringPresetsPresenter = new LeagueScoringPresetsPresenter();
|
||||
container.registerInstance(
|
||||
DI_TOKENS.ListLeagueScoringPresetsQuery,
|
||||
new ListLeagueScoringPresetsQuery(leagueScoringPresetProvider)
|
||||
DI_TOKENS.ListLeagueScoringPresetsUseCase,
|
||||
new ListLeagueScoringPresetsUseCase(leagueScoringPresetProvider, leagueScoringPresetsPresenter)
|
||||
);
|
||||
|
||||
const leagueScoringConfigPresenter = new LeagueScoringConfigPresenter();
|
||||
container.registerInstance(
|
||||
DI_TOKENS.GetLeagueScoringConfigQuery,
|
||||
new GetLeagueScoringConfigQuery(
|
||||
DI_TOKENS.GetLeagueScoringConfigUseCase,
|
||||
new GetLeagueScoringConfigUseCase(
|
||||
leagueRepository,
|
||||
seasonRepository,
|
||||
leagueScoringConfigRepository,
|
||||
gameRepository,
|
||||
leagueScoringPresetProvider
|
||||
leagueScoringPresetProvider,
|
||||
leagueScoringConfigPresenter
|
||||
)
|
||||
);
|
||||
|
||||
const leagueFullConfigPresenter = new LeagueFullConfigPresenter();
|
||||
container.registerInstance(
|
||||
DI_TOKENS.GetLeagueFullConfigQuery,
|
||||
new GetLeagueFullConfigQuery(
|
||||
DI_TOKENS.GetLeagueFullConfigUseCase,
|
||||
new GetLeagueFullConfigUseCase(
|
||||
leagueRepository,
|
||||
seasonRepository,
|
||||
leagueScoringConfigRepository,
|
||||
gameRepository
|
||||
gameRepository,
|
||||
leagueFullConfigPresenter
|
||||
)
|
||||
);
|
||||
|
||||
const leagueSchedulePreviewPresenter = new LeagueSchedulePreviewPresenter();
|
||||
container.registerInstance(
|
||||
DI_TOKENS.PreviewLeagueScheduleQuery,
|
||||
new PreviewLeagueScheduleQuery()
|
||||
DI_TOKENS.PreviewLeagueScheduleUseCase,
|
||||
new PreviewLeagueScheduleUseCase(undefined, leagueSchedulePreviewPresenter)
|
||||
);
|
||||
|
||||
const raceWithSOFPresenter = new RaceWithSOFPresenter();
|
||||
container.registerInstance(
|
||||
DI_TOKENS.GetRaceWithSOFQuery,
|
||||
new GetRaceWithSOFQuery(
|
||||
DI_TOKENS.GetRaceWithSOFUseCase,
|
||||
new GetRaceWithSOFUseCase(
|
||||
raceRepository,
|
||||
raceRegistrationRepository,
|
||||
resultRepository,
|
||||
driverRatingProvider
|
||||
driverRatingProvider,
|
||||
raceWithSOFPresenter
|
||||
)
|
||||
);
|
||||
|
||||
const leagueStatsPresenter = new LeagueStatsPresenter();
|
||||
container.registerInstance(
|
||||
DI_TOKENS.GetLeagueStatsQuery,
|
||||
new GetLeagueStatsQuery(
|
||||
DI_TOKENS.GetLeagueStatsUseCase,
|
||||
new GetLeagueStatsUseCase(
|
||||
leagueRepository,
|
||||
raceRepository,
|
||||
resultRepository,
|
||||
driverRatingProvider
|
||||
driverRatingProvider,
|
||||
leagueStatsPresenter
|
||||
)
|
||||
);
|
||||
|
||||
// Register queries - Teams
|
||||
const racesPresenter = new RacesPagePresenter();
|
||||
container.registerInstance(
|
||||
DI_TOKENS.GetAllTeamsQuery,
|
||||
new GetAllTeamsQuery(teamRepository)
|
||||
DI_TOKENS.GetRacesPageDataUseCase,
|
||||
new GetRacesPageDataUseCase(raceRepository, leagueRepository, racesPresenter)
|
||||
);
|
||||
|
||||
// Create services for driver leaderboard query
|
||||
const rankingService = {
|
||||
getAllDriverRankings: () => {
|
||||
const stats = getDIContainer().resolve<Record<string, any>>(DI_TOKENS.DriverStats);
|
||||
return Object.entries(stats).map(([driverId, stat]) => ({
|
||||
driverId,
|
||||
rating: stat.rating,
|
||||
overallRank: stat.overallRank,
|
||||
})).sort((a, b) => b.rating - a.rating);
|
||||
}
|
||||
};
|
||||
|
||||
const driverStatsService = {
|
||||
getDriverStats: (driverId: string) => {
|
||||
const stats = getDIContainer().resolve<Record<string, any>>(DI_TOKENS.DriverStats);
|
||||
return stats[driverId] || null;
|
||||
}
|
||||
};
|
||||
|
||||
const imageService = getDIContainer().resolve<ImageServicePort>(DI_TOKENS.ImageService);
|
||||
|
||||
const driversPresenter = new DriversLeaderboardPresenter();
|
||||
container.registerInstance(
|
||||
DI_TOKENS.GetTeamDetailsQuery,
|
||||
new GetTeamDetailsQuery(teamRepository, teamMembershipRepository)
|
||||
DI_TOKENS.GetDriversLeaderboardUseCase,
|
||||
new GetDriversLeaderboardUseCase(
|
||||
driverRepository,
|
||||
rankingService as any,
|
||||
driverStatsService as any,
|
||||
imageService,
|
||||
driversPresenter
|
||||
)
|
||||
);
|
||||
|
||||
const getDriverStatsAdapter = (driverId: string) => {
|
||||
const stats = getDIContainer().resolve<Record<string, any>>(DI_TOKENS.DriverStats);
|
||||
const stat = stats[driverId];
|
||||
if (!stat) return null;
|
||||
return {
|
||||
rating: stat.rating ?? null,
|
||||
wins: stat.wins ?? 0,
|
||||
totalRaces: stat.totalRaces ?? 0,
|
||||
};
|
||||
};
|
||||
|
||||
const teamsPresenter = new TeamsLeaderboardPresenter();
|
||||
container.registerInstance(
|
||||
DI_TOKENS.GetTeamMembersQuery,
|
||||
new GetTeamMembersQuery(teamMembershipRepository)
|
||||
DI_TOKENS.GetTeamsLeaderboardUseCase,
|
||||
new GetTeamsLeaderboardUseCase(
|
||||
teamRepository,
|
||||
teamMembershipRepository,
|
||||
driverRepository,
|
||||
getDriverStatsAdapter,
|
||||
teamsPresenter
|
||||
)
|
||||
);
|
||||
|
||||
// Register use cases - Teams (Query-like with Presenters)
|
||||
const allTeamsPresenter = new AllTeamsPresenter();
|
||||
container.registerInstance(
|
||||
DI_TOKENS.GetTeamJoinRequestsQuery,
|
||||
new GetTeamJoinRequestsQuery(teamMembershipRepository)
|
||||
DI_TOKENS.GetAllTeamsUseCase,
|
||||
new GetAllTeamsUseCase(teamRepository, teamMembershipRepository, allTeamsPresenter)
|
||||
);
|
||||
|
||||
const teamDetailsPresenter = new TeamDetailsPresenter();
|
||||
container.registerInstance(
|
||||
DI_TOKENS.GetDriverTeamQuery,
|
||||
new GetDriverTeamQuery(teamRepository, teamMembershipRepository)
|
||||
DI_TOKENS.GetTeamDetailsUseCase,
|
||||
new GetTeamDetailsUseCase(teamRepository, teamMembershipRepository, teamDetailsPresenter)
|
||||
);
|
||||
|
||||
const teamMembersPresenter = new TeamMembersPresenter();
|
||||
container.registerInstance(
|
||||
DI_TOKENS.GetTeamMembersUseCase,
|
||||
new GetTeamMembersUseCase(teamMembershipRepository, driverRepository, imageService, teamMembersPresenter)
|
||||
);
|
||||
|
||||
const teamJoinRequestsPresenter = new TeamJoinRequestsPresenter();
|
||||
container.registerInstance(
|
||||
DI_TOKENS.GetTeamJoinRequestsUseCase,
|
||||
new GetTeamJoinRequestsUseCase(teamMembershipRepository, driverRepository, imageService, teamJoinRequestsPresenter)
|
||||
);
|
||||
|
||||
const driverTeamPresenter = new DriverTeamPresenter();
|
||||
container.registerInstance(
|
||||
DI_TOKENS.GetDriverTeamUseCase,
|
||||
new GetDriverTeamUseCase(teamRepository, teamMembershipRepository, driverTeamPresenter)
|
||||
);
|
||||
|
||||
// Register queries - Stewarding
|
||||
const raceProtestsPresenter = new RaceProtestsPresenter();
|
||||
container.registerInstance(
|
||||
DI_TOKENS.GetRaceProtestsQuery,
|
||||
new GetRaceProtestsQuery(protestRepository, driverRepository)
|
||||
DI_TOKENS.GetRaceProtestsUseCase,
|
||||
new GetRaceProtestsUseCase(protestRepository, driverRepository, raceProtestsPresenter)
|
||||
);
|
||||
|
||||
const racePenaltiesPresenter = new RacePenaltiesPresenter();
|
||||
container.registerInstance(
|
||||
DI_TOKENS.GetRacePenaltiesQuery,
|
||||
new GetRacePenaltiesQuery(penaltyRepository, driverRepository)
|
||||
DI_TOKENS.GetRacePenaltiesUseCase,
|
||||
new GetRacePenaltiesUseCase(penaltyRepository, driverRepository, racePenaltiesPresenter)
|
||||
);
|
||||
|
||||
// Register queries - Notifications
|
||||
@@ -990,31 +1110,35 @@ export function configureDIContainer(): void {
|
||||
new GetUnreadNotificationsQuery(notificationRepository)
|
||||
);
|
||||
|
||||
// Register queries - Sponsors
|
||||
// Register use cases - Sponsors
|
||||
const sponsorRepository = container.resolve<ISponsorRepository>(DI_TOKENS.SponsorRepository);
|
||||
const seasonSponsorshipRepository = container.resolve<ISeasonSponsorshipRepository>(DI_TOKENS.SeasonSponsorshipRepository);
|
||||
|
||||
const sponsorDashboardPresenter = new SponsorDashboardPresenter();
|
||||
container.registerInstance(
|
||||
DI_TOKENS.GetSponsorDashboardQuery,
|
||||
new GetSponsorDashboardQuery(
|
||||
DI_TOKENS.GetSponsorDashboardUseCase,
|
||||
new GetSponsorDashboardUseCase(
|
||||
sponsorRepository,
|
||||
seasonSponsorshipRepository,
|
||||
seasonRepository,
|
||||
leagueRepository,
|
||||
leagueMembershipRepository,
|
||||
raceRepository
|
||||
raceRepository,
|
||||
sponsorDashboardPresenter
|
||||
)
|
||||
);
|
||||
|
||||
const sponsorSponsorshipsPresenter = new SponsorSponsorshipsPresenter();
|
||||
container.registerInstance(
|
||||
DI_TOKENS.GetSponsorSponsorshipsQuery,
|
||||
new GetSponsorSponsorshipsQuery(
|
||||
DI_TOKENS.GetSponsorSponsorshipsUseCase,
|
||||
new GetSponsorSponsorshipsUseCase(
|
||||
sponsorRepository,
|
||||
seasonSponsorshipRepository,
|
||||
seasonRepository,
|
||||
leagueRepository,
|
||||
leagueMembershipRepository,
|
||||
raceRepository
|
||||
raceRepository,
|
||||
sponsorSponsorshipsPresenter
|
||||
)
|
||||
);
|
||||
|
||||
@@ -1022,20 +1146,24 @@ export function configureDIContainer(): void {
|
||||
const sponsorshipRequestRepository = container.resolve<ISponsorshipRequestRepository>(DI_TOKENS.SponsorshipRequestRepository);
|
||||
const sponsorshipPricingRepository = container.resolve<ISponsorshipPricingRepository>(DI_TOKENS.SponsorshipPricingRepository);
|
||||
|
||||
const pendingSponsorshipRequestsPresenter = new PendingSponsorshipRequestsPresenter();
|
||||
container.registerInstance(
|
||||
DI_TOKENS.GetPendingSponsorshipRequestsQuery,
|
||||
new GetPendingSponsorshipRequestsQuery(
|
||||
DI_TOKENS.GetPendingSponsorshipRequestsUseCase,
|
||||
new GetPendingSponsorshipRequestsUseCase(
|
||||
sponsorshipRequestRepository,
|
||||
sponsorRepository
|
||||
sponsorRepository,
|
||||
pendingSponsorshipRequestsPresenter
|
||||
)
|
||||
);
|
||||
|
||||
const entitySponsorshipPricingPresenter = new EntitySponsorshipPricingPresenter();
|
||||
container.registerInstance(
|
||||
DI_TOKENS.GetEntitySponsorshipPricingQuery,
|
||||
new GetEntitySponsorshipPricingQuery(
|
||||
DI_TOKENS.GetEntitySponsorshipPricingUseCase,
|
||||
new GetEntitySponsorshipPricingUseCase(
|
||||
sponsorshipPricingRepository,
|
||||
sponsorshipRequestRepository,
|
||||
seasonSponsorshipRepository
|
||||
seasonSponsorshipRepository,
|
||||
entitySponsorshipPricingPresenter
|
||||
)
|
||||
);
|
||||
|
||||
|
||||
@@ -41,51 +41,54 @@ import type {
|
||||
JoinLeagueUseCase,
|
||||
RegisterForRaceUseCase,
|
||||
WithdrawFromRaceUseCase,
|
||||
IsDriverRegisteredForRaceQuery,
|
||||
GetRaceRegistrationsQuery,
|
||||
CreateTeamUseCase,
|
||||
JoinTeamUseCase,
|
||||
LeaveTeamUseCase,
|
||||
ApproveTeamJoinRequestUseCase,
|
||||
RejectTeamJoinRequestUseCase,
|
||||
UpdateTeamUseCase,
|
||||
GetAllTeamsQuery,
|
||||
GetTeamDetailsQuery,
|
||||
GetTeamMembersQuery,
|
||||
GetTeamJoinRequestsQuery,
|
||||
GetDriverTeamQuery,
|
||||
GetLeagueStandingsQuery,
|
||||
GetLeagueDriverSeasonStatsQuery,
|
||||
GetAllLeaguesWithCapacityQuery,
|
||||
GetAllLeaguesWithCapacityAndScoringQuery,
|
||||
ListLeagueScoringPresetsQuery,
|
||||
GetLeagueScoringConfigQuery,
|
||||
GetAllTeamsUseCase,
|
||||
GetTeamDetailsUseCase,
|
||||
GetTeamMembersUseCase,
|
||||
GetTeamJoinRequestsUseCase,
|
||||
GetDriverTeamUseCase,
|
||||
CreateLeagueWithSeasonAndScoringUseCase,
|
||||
GetLeagueFullConfigQuery,
|
||||
GetRaceWithSOFQuery,
|
||||
GetLeagueStatsQuery,
|
||||
FileProtestUseCase,
|
||||
ReviewProtestUseCase,
|
||||
ApplyPenaltyUseCase,
|
||||
GetRaceProtestsQuery,
|
||||
GetRacePenaltiesQuery,
|
||||
RequestProtestDefenseUseCase,
|
||||
SubmitProtestDefenseUseCase,
|
||||
GetSponsorDashboardQuery,
|
||||
GetSponsorSponsorshipsQuery,
|
||||
GetSponsorDashboardUseCase,
|
||||
GetSponsorSponsorshipsUseCase,
|
||||
ApplyForSponsorshipUseCase,
|
||||
AcceptSponsorshipRequestUseCase,
|
||||
RejectSponsorshipRequestUseCase,
|
||||
GetPendingSponsorshipRequestsQuery,
|
||||
GetEntitySponsorshipPricingQuery,
|
||||
GetPendingSponsorshipRequestsUseCase,
|
||||
GetEntitySponsorshipPricingUseCase,
|
||||
} from '@gridpilot/racing/application';
|
||||
import type { IsDriverRegisteredForRaceUseCase } from '@gridpilot/racing/application/use-cases/IsDriverRegisteredForRaceQuery';
|
||||
import type { GetRaceRegistrationsUseCase } from '@gridpilot/racing/application/use-cases/GetRaceRegistrationsQuery';
|
||||
import type { GetRaceWithSOFUseCase } from '@gridpilot/racing/application/use-cases/GetRaceWithSOFQuery';
|
||||
import type { GetRaceProtestsUseCase } from '@gridpilot/racing/application/use-cases/GetRaceProtestsQuery';
|
||||
import type { GetRacePenaltiesUseCase } from '@gridpilot/racing/application/use-cases/GetRacePenaltiesQuery';
|
||||
import type { GetRacesPageDataUseCase } from '@gridpilot/racing/application/use-cases/GetRacesPageDataUseCase';
|
||||
import type { GetDriversLeaderboardUseCase } from '@gridpilot/racing/application/use-cases/GetDriversLeaderboardUseCase';
|
||||
import type { GetTeamsLeaderboardUseCase } from '@gridpilot/racing/application/use-cases/GetTeamsLeaderboardUseCase';
|
||||
import type { GetLeagueStandingsUseCase } from '@gridpilot/racing/application/use-cases/GetLeagueStandingsQuery';
|
||||
import type { GetLeagueDriverSeasonStatsUseCase } from '@gridpilot/racing/application/use-cases/GetLeagueDriverSeasonStatsQuery';
|
||||
import type { GetAllLeaguesWithCapacityUseCase } from '@gridpilot/racing/application/use-cases/GetAllLeaguesWithCapacityQuery';
|
||||
import type { GetAllLeaguesWithCapacityAndScoringUseCase } from '@gridpilot/racing/application/use-cases/GetAllLeaguesWithCapacityAndScoringQuery';
|
||||
import type { ListLeagueScoringPresetsUseCase } from '@gridpilot/racing/application/use-cases/ListLeagueScoringPresetsQuery';
|
||||
import type { GetLeagueScoringConfigUseCase } from '@gridpilot/racing/application/use-cases/GetLeagueScoringConfigQuery';
|
||||
import type { GetLeagueFullConfigUseCase } from '@gridpilot/racing/application/use-cases/GetLeagueFullConfigQuery';
|
||||
import type { GetLeagueStatsUseCase } from '@gridpilot/racing/application/use-cases/GetLeagueStatsQuery';
|
||||
import type { ISponsorRepository } from '@gridpilot/racing/domain/repositories/ISponsorRepository';
|
||||
import type { ISeasonSponsorshipRepository } from '@gridpilot/racing/domain/repositories/ISeasonSponsorshipRepository';
|
||||
import type { ISponsorshipRequestRepository } from '@gridpilot/racing/domain/repositories/ISponsorshipRequestRepository';
|
||||
import type { ISponsorshipPricingRepository } from '@gridpilot/racing/domain/repositories/ISponsorshipPricingRepository';
|
||||
import type { TransferLeagueOwnershipUseCase } from '@gridpilot/racing/application/use-cases/TransferLeagueOwnershipUseCase';
|
||||
import type { DriverRatingProvider } from '@gridpilot/racing/application';
|
||||
import type { PreviewLeagueScheduleQuery } from '@gridpilot/racing/application';
|
||||
import type { PreviewLeagueScheduleUseCase } from '@gridpilot/racing/application';
|
||||
import type { LeagueScoringPresetProvider } from '@gridpilot/racing/application/ports/LeagueScoringPresetProvider';
|
||||
import { createDemoDriverStats, getDemoLeagueRankings, type DriverStats } from '@gridpilot/testing-support';
|
||||
|
||||
@@ -211,64 +214,79 @@ class DIContainer {
|
||||
return getDIContainer().resolve<WithdrawFromRaceUseCase>(DI_TOKENS.WithdrawFromRaceUseCase);
|
||||
}
|
||||
|
||||
get isDriverRegisteredForRaceQuery(): IsDriverRegisteredForRaceQuery {
|
||||
get isDriverRegisteredForRaceUseCase(): IsDriverRegisteredForRaceUseCase {
|
||||
this.ensureInitialized();
|
||||
return getDIContainer().resolve<IsDriverRegisteredForRaceQuery>(DI_TOKENS.IsDriverRegisteredForRaceQuery);
|
||||
return getDIContainer().resolve<IsDriverRegisteredForRaceUseCase>(DI_TOKENS.IsDriverRegisteredForRaceUseCase);
|
||||
}
|
||||
|
||||
get getRaceRegistrationsQuery(): GetRaceRegistrationsQuery {
|
||||
get getRaceRegistrationsUseCase(): GetRaceRegistrationsUseCase {
|
||||
this.ensureInitialized();
|
||||
return getDIContainer().resolve<GetRaceRegistrationsQuery>(DI_TOKENS.GetRaceRegistrationsQuery);
|
||||
return getDIContainer().resolve<GetRaceRegistrationsUseCase>(DI_TOKENS.GetRaceRegistrationsUseCase);
|
||||
}
|
||||
|
||||
get getLeagueStandingsQuery(): GetLeagueStandingsQuery {
|
||||
get getLeagueStandingsUseCase(): GetLeagueStandingsUseCase {
|
||||
this.ensureInitialized();
|
||||
return getDIContainer().resolve<GetLeagueStandingsQuery>(DI_TOKENS.GetLeagueStandingsQuery);
|
||||
return getDIContainer().resolve<GetLeagueStandingsUseCase>(DI_TOKENS.GetLeagueStandingsUseCase);
|
||||
}
|
||||
|
||||
get getLeagueDriverSeasonStatsQuery(): GetLeagueDriverSeasonStatsQuery {
|
||||
get getLeagueDriverSeasonStatsUseCase(): GetLeagueDriverSeasonStatsUseCase {
|
||||
this.ensureInitialized();
|
||||
return getDIContainer().resolve<GetLeagueDriverSeasonStatsQuery>(DI_TOKENS.GetLeagueDriverSeasonStatsQuery);
|
||||
return getDIContainer().resolve<GetLeagueDriverSeasonStatsUseCase>(DI_TOKENS.GetLeagueDriverSeasonStatsUseCase);
|
||||
}
|
||||
|
||||
get getAllLeaguesWithCapacityQuery(): GetAllLeaguesWithCapacityQuery {
|
||||
get getAllLeaguesWithCapacityUseCase(): GetAllLeaguesWithCapacityUseCase {
|
||||
this.ensureInitialized();
|
||||
return getDIContainer().resolve<GetAllLeaguesWithCapacityQuery>(DI_TOKENS.GetAllLeaguesWithCapacityQuery);
|
||||
return getDIContainer().resolve<GetAllLeaguesWithCapacityUseCase>(DI_TOKENS.GetAllLeaguesWithCapacityUseCase);
|
||||
}
|
||||
|
||||
get getAllLeaguesWithCapacityAndScoringQuery(): GetAllLeaguesWithCapacityAndScoringQuery {
|
||||
get getAllLeaguesWithCapacityAndScoringUseCase(): GetAllLeaguesWithCapacityAndScoringUseCase {
|
||||
this.ensureInitialized();
|
||||
return getDIContainer().resolve<GetAllLeaguesWithCapacityAndScoringQuery>(DI_TOKENS.GetAllLeaguesWithCapacityAndScoringQuery);
|
||||
return getDIContainer().resolve<GetAllLeaguesWithCapacityAndScoringUseCase>(DI_TOKENS.GetAllLeaguesWithCapacityAndScoringUseCase);
|
||||
}
|
||||
|
||||
get listLeagueScoringPresetsQuery(): ListLeagueScoringPresetsQuery {
|
||||
get listLeagueScoringPresetsUseCase(): ListLeagueScoringPresetsUseCase {
|
||||
this.ensureInitialized();
|
||||
return getDIContainer().resolve<ListLeagueScoringPresetsQuery>(DI_TOKENS.ListLeagueScoringPresetsQuery);
|
||||
return getDIContainer().resolve<ListLeagueScoringPresetsUseCase>(DI_TOKENS.ListLeagueScoringPresetsUseCase);
|
||||
}
|
||||
|
||||
get getLeagueScoringConfigQuery(): GetLeagueScoringConfigQuery {
|
||||
get getLeagueScoringConfigUseCase(): GetLeagueScoringConfigUseCase {
|
||||
this.ensureInitialized();
|
||||
return getDIContainer().resolve<GetLeagueScoringConfigQuery>(DI_TOKENS.GetLeagueScoringConfigQuery);
|
||||
return getDIContainer().resolve<GetLeagueScoringConfigUseCase>(DI_TOKENS.GetLeagueScoringConfigUseCase);
|
||||
}
|
||||
|
||||
get getLeagueFullConfigQuery(): GetLeagueFullConfigQuery {
|
||||
get getLeagueFullConfigUseCase(): GetLeagueFullConfigUseCase {
|
||||
this.ensureInitialized();
|
||||
return getDIContainer().resolve<GetLeagueFullConfigQuery>(DI_TOKENS.GetLeagueFullConfigQuery);
|
||||
return getDIContainer().resolve<GetLeagueFullConfigUseCase>(DI_TOKENS.GetLeagueFullConfigUseCase);
|
||||
}
|
||||
|
||||
get previewLeagueScheduleQuery(): PreviewLeagueScheduleQuery {
|
||||
get previewLeagueScheduleUseCase(): PreviewLeagueScheduleUseCase {
|
||||
this.ensureInitialized();
|
||||
return getDIContainer().resolve<PreviewLeagueScheduleQuery>(DI_TOKENS.PreviewLeagueScheduleQuery);
|
||||
return getDIContainer().resolve<PreviewLeagueScheduleUseCase>(DI_TOKENS.PreviewLeagueScheduleUseCase);
|
||||
}
|
||||
|
||||
get getRaceWithSOFQuery(): GetRaceWithSOFQuery {
|
||||
get getRaceWithSOFUseCase(): GetRaceWithSOFUseCase {
|
||||
this.ensureInitialized();
|
||||
return getDIContainer().resolve<GetRaceWithSOFQuery>(DI_TOKENS.GetRaceWithSOFQuery);
|
||||
return getDIContainer().resolve<GetRaceWithSOFUseCase>(DI_TOKENS.GetRaceWithSOFUseCase);
|
||||
}
|
||||
|
||||
get getLeagueStatsQuery(): GetLeagueStatsQuery {
|
||||
get getLeagueStatsUseCase(): GetLeagueStatsUseCase {
|
||||
this.ensureInitialized();
|
||||
return getDIContainer().resolve<GetLeagueStatsQuery>(DI_TOKENS.GetLeagueStatsQuery);
|
||||
return getDIContainer().resolve<GetLeagueStatsUseCase>(DI_TOKENS.GetLeagueStatsUseCase);
|
||||
}
|
||||
|
||||
get getRacesPageDataUseCase(): GetRacesPageDataUseCase {
|
||||
this.ensureInitialized();
|
||||
return getDIContainer().resolve<GetRacesPageDataUseCase>(DI_TOKENS.GetRacesPageDataUseCase);
|
||||
}
|
||||
|
||||
get getDriversLeaderboardUseCase(): GetDriversLeaderboardUseCase {
|
||||
this.ensureInitialized();
|
||||
return getDIContainer().resolve<GetDriversLeaderboardUseCase>(DI_TOKENS.GetDriversLeaderboardUseCase);
|
||||
}
|
||||
|
||||
get getTeamsLeaderboardUseCase(): GetTeamsLeaderboardUseCase {
|
||||
this.ensureInitialized();
|
||||
return getDIContainer().resolve<GetTeamsLeaderboardUseCase>(DI_TOKENS.GetTeamsLeaderboardUseCase);
|
||||
}
|
||||
|
||||
get driverRatingProvider(): DriverRatingProvider {
|
||||
@@ -311,29 +329,29 @@ class DIContainer {
|
||||
return getDIContainer().resolve<UpdateTeamUseCase>(DI_TOKENS.UpdateTeamUseCase);
|
||||
}
|
||||
|
||||
get getAllTeamsQuery(): GetAllTeamsQuery {
|
||||
get getAllTeamsUseCase(): GetAllTeamsUseCase {
|
||||
this.ensureInitialized();
|
||||
return getDIContainer().resolve<GetAllTeamsQuery>(DI_TOKENS.GetAllTeamsQuery);
|
||||
return getDIContainer().resolve<GetAllTeamsUseCase>(DI_TOKENS.GetAllTeamsUseCase);
|
||||
}
|
||||
|
||||
get getTeamDetailsQuery(): GetTeamDetailsQuery {
|
||||
get getTeamDetailsUseCase(): GetTeamDetailsUseCase {
|
||||
this.ensureInitialized();
|
||||
return getDIContainer().resolve<GetTeamDetailsQuery>(DI_TOKENS.GetTeamDetailsQuery);
|
||||
return getDIContainer().resolve<GetTeamDetailsUseCase>(DI_TOKENS.GetTeamDetailsUseCase);
|
||||
}
|
||||
|
||||
get getTeamMembersQuery(): GetTeamMembersQuery {
|
||||
get getTeamMembersUseCase(): GetTeamMembersUseCase {
|
||||
this.ensureInitialized();
|
||||
return getDIContainer().resolve<GetTeamMembersQuery>(DI_TOKENS.GetTeamMembersQuery);
|
||||
return getDIContainer().resolve<GetTeamMembersUseCase>(DI_TOKENS.GetTeamMembersUseCase);
|
||||
}
|
||||
|
||||
get getTeamJoinRequestsQuery(): GetTeamJoinRequestsQuery {
|
||||
get getTeamJoinRequestsUseCase(): GetTeamJoinRequestsUseCase {
|
||||
this.ensureInitialized();
|
||||
return getDIContainer().resolve<GetTeamJoinRequestsQuery>(DI_TOKENS.GetTeamJoinRequestsQuery);
|
||||
return getDIContainer().resolve<GetTeamJoinRequestsUseCase>(DI_TOKENS.GetTeamJoinRequestsUseCase);
|
||||
}
|
||||
|
||||
get getDriverTeamQuery(): GetDriverTeamQuery {
|
||||
get getDriverTeamUseCase(): GetDriverTeamUseCase {
|
||||
this.ensureInitialized();
|
||||
return getDIContainer().resolve<GetDriverTeamQuery>(DI_TOKENS.GetDriverTeamQuery);
|
||||
return getDIContainer().resolve<GetDriverTeamUseCase>(DI_TOKENS.GetDriverTeamUseCase);
|
||||
}
|
||||
|
||||
get teamRepository(): ITeamRepository {
|
||||
@@ -411,14 +429,14 @@ class DIContainer {
|
||||
return getDIContainer().resolve<ApplyPenaltyUseCase>(DI_TOKENS.ApplyPenaltyUseCase);
|
||||
}
|
||||
|
||||
get getRaceProtestsQuery(): GetRaceProtestsQuery {
|
||||
get getRaceProtestsUseCase(): GetRaceProtestsUseCase {
|
||||
this.ensureInitialized();
|
||||
return getDIContainer().resolve<GetRaceProtestsQuery>(DI_TOKENS.GetRaceProtestsQuery);
|
||||
return getDIContainer().resolve<GetRaceProtestsUseCase>(DI_TOKENS.GetRaceProtestsUseCase);
|
||||
}
|
||||
|
||||
get getRacePenaltiesQuery(): GetRacePenaltiesQuery {
|
||||
get getRacePenaltiesUseCase(): GetRacePenaltiesUseCase {
|
||||
this.ensureInitialized();
|
||||
return getDIContainer().resolve<GetRacePenaltiesQuery>(DI_TOKENS.GetRacePenaltiesQuery);
|
||||
return getDIContainer().resolve<GetRacePenaltiesUseCase>(DI_TOKENS.GetRacePenaltiesUseCase);
|
||||
}
|
||||
|
||||
get requestProtestDefenseUseCase(): RequestProtestDefenseUseCase {
|
||||
@@ -446,14 +464,14 @@ class DIContainer {
|
||||
return getDIContainer().resolve<ISeasonSponsorshipRepository>(DI_TOKENS.SeasonSponsorshipRepository);
|
||||
}
|
||||
|
||||
get getSponsorDashboardQuery(): GetSponsorDashboardQuery {
|
||||
get getSponsorDashboardUseCase(): GetSponsorDashboardUseCase {
|
||||
this.ensureInitialized();
|
||||
return getDIContainer().resolve<GetSponsorDashboardQuery>(DI_TOKENS.GetSponsorDashboardQuery);
|
||||
return getDIContainer().resolve<GetSponsorDashboardUseCase>(DI_TOKENS.GetSponsorDashboardUseCase);
|
||||
}
|
||||
|
||||
get getSponsorSponsorshipsQuery(): GetSponsorSponsorshipsQuery {
|
||||
get getSponsorSponsorshipsUseCase(): GetSponsorSponsorshipsUseCase {
|
||||
this.ensureInitialized();
|
||||
return getDIContainer().resolve<GetSponsorSponsorshipsQuery>(DI_TOKENS.GetSponsorSponsorshipsQuery);
|
||||
return getDIContainer().resolve<GetSponsorSponsorshipsUseCase>(DI_TOKENS.GetSponsorSponsorshipsUseCase);
|
||||
}
|
||||
|
||||
get sponsorshipRequestRepository(): ISponsorshipRequestRepository {
|
||||
@@ -481,14 +499,14 @@ class DIContainer {
|
||||
return getDIContainer().resolve<RejectSponsorshipRequestUseCase>(DI_TOKENS.RejectSponsorshipRequestUseCase);
|
||||
}
|
||||
|
||||
get getPendingSponsorshipRequestsQuery(): GetPendingSponsorshipRequestsQuery {
|
||||
get getPendingSponsorshipRequestsUseCase(): GetPendingSponsorshipRequestsUseCase {
|
||||
this.ensureInitialized();
|
||||
return getDIContainer().resolve<GetPendingSponsorshipRequestsQuery>(DI_TOKENS.GetPendingSponsorshipRequestsQuery);
|
||||
return getDIContainer().resolve<GetPendingSponsorshipRequestsUseCase>(DI_TOKENS.GetPendingSponsorshipRequestsUseCase);
|
||||
}
|
||||
|
||||
get getEntitySponsorshipPricingQuery(): GetEntitySponsorshipPricingQuery {
|
||||
get getEntitySponsorshipPricingUseCase(): GetEntitySponsorshipPricingUseCase {
|
||||
this.ensureInitialized();
|
||||
return getDIContainer().resolve<GetEntitySponsorshipPricingQuery>(DI_TOKENS.GetEntitySponsorshipPricingQuery);
|
||||
return getDIContainer().resolve<GetEntitySponsorshipPricingUseCase>(DI_TOKENS.GetEntitySponsorshipPricingUseCase);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -543,57 +561,68 @@ export function getWithdrawFromRaceUseCase(): WithdrawFromRaceUseCase {
|
||||
return DIContainer.getInstance().withdrawFromRaceUseCase;
|
||||
}
|
||||
|
||||
export function getIsDriverRegisteredForRaceQuery(): IsDriverRegisteredForRaceQuery {
|
||||
return DIContainer.getInstance().isDriverRegisteredForRaceQuery;
|
||||
export function getIsDriverRegisteredForRaceUseCase(): IsDriverRegisteredForRaceUseCase {
|
||||
return DIContainer.getInstance().isDriverRegisteredForRaceUseCase;
|
||||
}
|
||||
|
||||
export function getGetRaceRegistrationsQuery(): GetRaceRegistrationsQuery {
|
||||
return DIContainer.getInstance().getRaceRegistrationsQuery;
|
||||
export function getGetRaceRegistrationsUseCase(): GetRaceRegistrationsUseCase {
|
||||
return DIContainer.getInstance().getRaceRegistrationsUseCase;
|
||||
}
|
||||
|
||||
export function getGetLeagueStandingsQuery(): GetLeagueStandingsQuery {
|
||||
return DIContainer.getInstance().getLeagueStandingsQuery;
|
||||
export function getGetLeagueStandingsUseCase(): GetLeagueStandingsUseCase {
|
||||
return DIContainer.getInstance().getLeagueStandingsUseCase;
|
||||
}
|
||||
|
||||
export function getGetLeagueDriverSeasonStatsQuery(): GetLeagueDriverSeasonStatsQuery {
|
||||
return DIContainer.getInstance().getLeagueDriverSeasonStatsQuery;
|
||||
export function getGetLeagueDriverSeasonStatsUseCase(): GetLeagueDriverSeasonStatsUseCase {
|
||||
return DIContainer.getInstance().getLeagueDriverSeasonStatsUseCase;
|
||||
}
|
||||
|
||||
export function getGetAllLeaguesWithCapacityQuery(): GetAllLeaguesWithCapacityQuery {
|
||||
return DIContainer.getInstance().getAllLeaguesWithCapacityQuery;
|
||||
export function getGetAllLeaguesWithCapacityUseCase(): GetAllLeaguesWithCapacityUseCase {
|
||||
return DIContainer.getInstance().getAllLeaguesWithCapacityUseCase;
|
||||
}
|
||||
|
||||
export function getGetAllLeaguesWithCapacityAndScoringQuery(): GetAllLeaguesWithCapacityAndScoringQuery {
|
||||
return DIContainer.getInstance().getAllLeaguesWithCapacityAndScoringQuery;
|
||||
export function getGetAllLeaguesWithCapacityAndScoringUseCase(): GetAllLeaguesWithCapacityAndScoringUseCase {
|
||||
return DIContainer.getInstance().getAllLeaguesWithCapacityAndScoringUseCase;
|
||||
}
|
||||
|
||||
export function getGetLeagueScoringConfigQuery(): GetLeagueScoringConfigQuery {
|
||||
return DIContainer.getInstance().getLeagueScoringConfigQuery;
|
||||
export function getGetLeagueScoringConfigUseCase(): GetLeagueScoringConfigUseCase {
|
||||
return DIContainer.getInstance().getLeagueScoringConfigUseCase;
|
||||
}
|
||||
|
||||
export function getGetLeagueFullConfigQuery(): GetLeagueFullConfigQuery {
|
||||
return DIContainer.getInstance().getLeagueFullConfigQuery;
|
||||
export function getGetLeagueFullConfigUseCase(): GetLeagueFullConfigUseCase {
|
||||
return DIContainer.getInstance().getLeagueFullConfigUseCase;
|
||||
}
|
||||
|
||||
// Placeholder export for future schedule preview API wiring.
|
||||
export function getPreviewLeagueScheduleQuery(): PreviewLeagueScheduleQuery {
|
||||
return DIContainer.getInstance().previewLeagueScheduleQuery;
|
||||
export function getPreviewLeagueScheduleUseCase(): PreviewLeagueScheduleUseCase {
|
||||
return DIContainer.getInstance().previewLeagueScheduleUseCase;
|
||||
}
|
||||
|
||||
export function getListLeagueScoringPresetsQuery(): ListLeagueScoringPresetsQuery {
|
||||
return DIContainer.getInstance().listLeagueScoringPresetsQuery;
|
||||
export function getListLeagueScoringPresetsUseCase(): ListLeagueScoringPresetsUseCase {
|
||||
return DIContainer.getInstance().listLeagueScoringPresetsUseCase;
|
||||
}
|
||||
|
||||
export function getCreateLeagueWithSeasonAndScoringUseCase(): CreateLeagueWithSeasonAndScoringUseCase {
|
||||
return DIContainer.getInstance().createLeagueWithSeasonAndScoringUseCase;
|
||||
}
|
||||
|
||||
export function getGetRaceWithSOFQuery(): GetRaceWithSOFQuery {
|
||||
return DIContainer.getInstance().getRaceWithSOFQuery;
|
||||
export function getGetRaceWithSOFUseCase(): GetRaceWithSOFUseCase {
|
||||
return DIContainer.getInstance().getRaceWithSOFUseCase;
|
||||
}
|
||||
|
||||
export function getGetLeagueStatsQuery(): GetLeagueStatsQuery {
|
||||
return DIContainer.getInstance().getLeagueStatsQuery;
|
||||
export function getGetLeagueStatsUseCase(): GetLeagueStatsUseCase {
|
||||
return DIContainer.getInstance().getLeagueStatsUseCase;
|
||||
}
|
||||
|
||||
export function getGetRacesPageDataUseCase(): GetRacesPageDataUseCase {
|
||||
return DIContainer.getInstance().getRacesPageDataUseCase;
|
||||
}
|
||||
|
||||
export function getGetDriversLeaderboardUseCase(): GetDriversLeaderboardUseCase {
|
||||
return DIContainer.getInstance().getDriversLeaderboardUseCase;
|
||||
}
|
||||
|
||||
export function getGetTeamsLeaderboardUseCase(): GetTeamsLeaderboardUseCase {
|
||||
return DIContainer.getInstance().getTeamsLeaderboardUseCase;
|
||||
}
|
||||
|
||||
export function getDriverRatingProvider(): DriverRatingProvider {
|
||||
@@ -632,24 +661,24 @@ export function getUpdateTeamUseCase(): UpdateTeamUseCase {
|
||||
return DIContainer.getInstance().updateTeamUseCase;
|
||||
}
|
||||
|
||||
export function getGetAllTeamsQuery(): GetAllTeamsQuery {
|
||||
return DIContainer.getInstance().getAllTeamsQuery;
|
||||
export function getGetAllTeamsUseCase(): GetAllTeamsUseCase {
|
||||
return DIContainer.getInstance().getAllTeamsUseCase;
|
||||
}
|
||||
|
||||
export function getGetTeamDetailsQuery(): GetTeamDetailsQuery {
|
||||
return DIContainer.getInstance().getTeamDetailsQuery;
|
||||
export function getGetTeamDetailsUseCase(): GetTeamDetailsUseCase {
|
||||
return DIContainer.getInstance().getTeamDetailsUseCase;
|
||||
}
|
||||
|
||||
export function getGetTeamMembersQuery(): GetTeamMembersQuery {
|
||||
return DIContainer.getInstance().getTeamMembersQuery;
|
||||
export function getGetTeamMembersUseCase(): GetTeamMembersUseCase {
|
||||
return DIContainer.getInstance().getTeamMembersUseCase;
|
||||
}
|
||||
|
||||
export function getGetTeamJoinRequestsQuery(): GetTeamJoinRequestsQuery {
|
||||
return DIContainer.getInstance().getTeamJoinRequestsQuery;
|
||||
export function getGetTeamJoinRequestsUseCase(): GetTeamJoinRequestsUseCase {
|
||||
return DIContainer.getInstance().getTeamJoinRequestsUseCase;
|
||||
}
|
||||
|
||||
export function getGetDriverTeamQuery(): GetDriverTeamQuery {
|
||||
return DIContainer.getInstance().getDriverTeamQuery;
|
||||
export function getGetDriverTeamUseCase(): GetDriverTeamUseCase {
|
||||
return DIContainer.getInstance().getDriverTeamUseCase;
|
||||
}
|
||||
|
||||
export function getFeedRepository(): IFeedRepository {
|
||||
@@ -708,12 +737,12 @@ export function getApplyPenaltyUseCase(): ApplyPenaltyUseCase {
|
||||
return DIContainer.getInstance().applyPenaltyUseCase;
|
||||
}
|
||||
|
||||
export function getGetRaceProtestsQuery(): GetRaceProtestsQuery {
|
||||
return DIContainer.getInstance().getRaceProtestsQuery;
|
||||
export function getGetRaceProtestsUseCase(): GetRaceProtestsUseCase {
|
||||
return DIContainer.getInstance().getRaceProtestsUseCase;
|
||||
}
|
||||
|
||||
export function getGetRacePenaltiesQuery(): GetRacePenaltiesQuery {
|
||||
return DIContainer.getInstance().getRacePenaltiesQuery;
|
||||
export function getGetRacePenaltiesUseCase(): GetRacePenaltiesUseCase {
|
||||
return DIContainer.getInstance().getRacePenaltiesUseCase;
|
||||
}
|
||||
|
||||
export function getRequestProtestDefenseUseCase(): RequestProtestDefenseUseCase {
|
||||
@@ -736,12 +765,12 @@ export function getSeasonSponsorshipRepository(): ISeasonSponsorshipRepository {
|
||||
return DIContainer.getInstance().seasonSponsorshipRepository;
|
||||
}
|
||||
|
||||
export function getGetSponsorDashboardQuery(): GetSponsorDashboardQuery {
|
||||
return DIContainer.getInstance().getSponsorDashboardQuery;
|
||||
export function getGetSponsorDashboardUseCase(): GetSponsorDashboardUseCase {
|
||||
return DIContainer.getInstance().getSponsorDashboardUseCase;
|
||||
}
|
||||
|
||||
export function getGetSponsorSponsorshipsQuery(): GetSponsorSponsorshipsQuery {
|
||||
return DIContainer.getInstance().getSponsorSponsorshipsQuery;
|
||||
export function getGetSponsorSponsorshipsUseCase(): GetSponsorSponsorshipsUseCase {
|
||||
return DIContainer.getInstance().getSponsorSponsorshipsUseCase;
|
||||
}
|
||||
|
||||
export function getSponsorshipRequestRepository(): ISponsorshipRequestRepository {
|
||||
@@ -764,12 +793,12 @@ export function getRejectSponsorshipRequestUseCase(): RejectSponsorshipRequestUs
|
||||
return DIContainer.getInstance().rejectSponsorshipRequestUseCase;
|
||||
}
|
||||
|
||||
export function getGetPendingSponsorshipRequestsQuery(): GetPendingSponsorshipRequestsQuery {
|
||||
return DIContainer.getInstance().getPendingSponsorshipRequestsQuery;
|
||||
export function getGetPendingSponsorshipRequestsUseCase(): GetPendingSponsorshipRequestsUseCase {
|
||||
return DIContainer.getInstance().getPendingSponsorshipRequestsUseCase;
|
||||
}
|
||||
|
||||
export function getGetEntitySponsorshipPricingQuery(): GetEntitySponsorshipPricingQuery {
|
||||
return DIContainer.getInstance().getEntitySponsorshipPricingQuery;
|
||||
export function getGetEntitySponsorshipPricingUseCase(): GetEntitySponsorshipPricingUseCase {
|
||||
return DIContainer.getInstance().getEntitySponsorshipPricingUseCase;
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -64,38 +64,41 @@ export const DI_TOKENS = {
|
||||
MarkNotificationReadUseCase: Symbol.for('MarkNotificationReadUseCase'),
|
||||
|
||||
// Queries - Racing
|
||||
IsDriverRegisteredForRaceQuery: Symbol.for('IsDriverRegisteredForRaceQuery'),
|
||||
GetRaceRegistrationsQuery: Symbol.for('GetRaceRegistrationsQuery'),
|
||||
GetLeagueStandingsQuery: Symbol.for('GetLeagueStandingsQuery'),
|
||||
GetLeagueDriverSeasonStatsQuery: Symbol.for('GetLeagueDriverSeasonStatsQuery'),
|
||||
GetAllLeaguesWithCapacityQuery: Symbol.for('GetAllLeaguesWithCapacityQuery'),
|
||||
GetAllLeaguesWithCapacityAndScoringQuery: Symbol.for('GetAllLeaguesWithCapacityAndScoringQuery'),
|
||||
ListLeagueScoringPresetsQuery: Symbol.for('ListLeagueScoringPresetsQuery'),
|
||||
GetLeagueScoringConfigQuery: Symbol.for('GetLeagueScoringConfigQuery'),
|
||||
GetLeagueFullConfigQuery: Symbol.for('GetLeagueFullConfigQuery'),
|
||||
PreviewLeagueScheduleQuery: Symbol.for('PreviewLeagueScheduleQuery'),
|
||||
GetRaceWithSOFQuery: Symbol.for('GetRaceWithSOFQuery'),
|
||||
GetLeagueStatsQuery: Symbol.for('GetLeagueStatsQuery'),
|
||||
IsDriverRegisteredForRaceUseCase: Symbol.for('IsDriverRegisteredForRaceUseCase'),
|
||||
GetRaceRegistrationsUseCase: Symbol.for('GetRaceRegistrationsUseCase'),
|
||||
GetLeagueStandingsUseCase: Symbol.for('GetLeagueStandingsUseCase'),
|
||||
GetLeagueDriverSeasonStatsUseCase: Symbol.for('GetLeagueDriverSeasonStatsUseCase'),
|
||||
GetAllLeaguesWithCapacityUseCase: Symbol.for('GetAllLeaguesWithCapacityUseCase'),
|
||||
GetAllLeaguesWithCapacityAndScoringUseCase: Symbol.for('GetAllLeaguesWithCapacityAndScoringUseCase'),
|
||||
ListLeagueScoringPresetsUseCase: Symbol.for('ListLeagueScoringPresetsUseCase'),
|
||||
GetLeagueScoringConfigUseCase: Symbol.for('GetLeagueScoringConfigUseCase'),
|
||||
GetLeagueFullConfigUseCase: Symbol.for('GetLeagueFullConfigUseCase'),
|
||||
PreviewLeagueScheduleUseCase: Symbol.for('PreviewLeagueScheduleUseCase'),
|
||||
GetRaceWithSOFUseCase: Symbol.for('GetRaceWithSOFUseCase'),
|
||||
GetLeagueStatsUseCase: Symbol.for('GetLeagueStatsUseCase'),
|
||||
GetRacesPageDataUseCase: Symbol.for('GetRacesPageDataUseCase'),
|
||||
GetDriversLeaderboardUseCase: Symbol.for('GetDriversLeaderboardUseCase'),
|
||||
GetTeamsLeaderboardUseCase: Symbol.for('GetTeamsLeaderboardUseCase'),
|
||||
|
||||
// Queries - Teams
|
||||
GetAllTeamsQuery: Symbol.for('GetAllTeamsQuery'),
|
||||
GetTeamDetailsQuery: Symbol.for('GetTeamDetailsQuery'),
|
||||
GetTeamMembersQuery: Symbol.for('GetTeamMembersQuery'),
|
||||
GetTeamJoinRequestsQuery: Symbol.for('GetTeamJoinRequestsQuery'),
|
||||
GetDriverTeamQuery: Symbol.for('GetDriverTeamQuery'),
|
||||
// Use Cases - Teams (Query-like)
|
||||
GetAllTeamsUseCase: Symbol.for('GetAllTeamsUseCase'),
|
||||
GetTeamDetailsUseCase: Symbol.for('GetTeamDetailsUseCase'),
|
||||
GetTeamMembersUseCase: Symbol.for('GetTeamMembersUseCase'),
|
||||
GetTeamJoinRequestsUseCase: Symbol.for('GetTeamJoinRequestsUseCase'),
|
||||
GetDriverTeamUseCase: Symbol.for('GetDriverTeamUseCase'),
|
||||
|
||||
// Queries - Stewarding
|
||||
GetRaceProtestsQuery: Symbol.for('GetRaceProtestsQuery'),
|
||||
GetRacePenaltiesQuery: Symbol.for('GetRacePenaltiesQuery'),
|
||||
GetRaceProtestsUseCase: Symbol.for('GetRaceProtestsUseCase'),
|
||||
GetRacePenaltiesUseCase: Symbol.for('GetRacePenaltiesUseCase'),
|
||||
|
||||
// Queries - Notifications
|
||||
GetUnreadNotificationsQuery: Symbol.for('GetUnreadNotificationsQuery'),
|
||||
|
||||
// Queries - Sponsors
|
||||
GetSponsorDashboardQuery: Symbol.for('GetSponsorDashboardQuery'),
|
||||
GetSponsorSponsorshipsQuery: Symbol.for('GetSponsorSponsorshipsQuery'),
|
||||
GetPendingSponsorshipRequestsQuery: Symbol.for('GetPendingSponsorshipRequestsQuery'),
|
||||
GetEntitySponsorshipPricingQuery: Symbol.for('GetEntitySponsorshipPricingQuery'),
|
||||
// Use Cases - Sponsors
|
||||
GetSponsorDashboardUseCase: Symbol.for('GetSponsorDashboardUseCase'),
|
||||
GetSponsorSponsorshipsUseCase: Symbol.for('GetSponsorSponsorshipsUseCase'),
|
||||
GetPendingSponsorshipRequestsUseCase: Symbol.for('GetPendingSponsorshipRequestsUseCase'),
|
||||
GetEntitySponsorshipPricingUseCase: Symbol.for('GetEntitySponsorshipPricingUseCase'),
|
||||
|
||||
// Use Cases - Sponsorship
|
||||
ApplyForSponsorshipUseCase: Symbol.for('ApplyForSponsorshipUseCase'),
|
||||
@@ -104,6 +107,20 @@ export const DI_TOKENS = {
|
||||
|
||||
// Data
|
||||
DriverStats: Symbol.for('DriverStats'),
|
||||
|
||||
// Presenters - Racing
|
||||
RaceWithSOFPresenter: Symbol.for('IRaceWithSOFPresenter'),
|
||||
RaceProtestsPresenter: Symbol.for('IRaceProtestsPresenter'),
|
||||
RacePenaltiesPresenter: Symbol.for('IRacePenaltiesPresenter'),
|
||||
RaceRegistrationsPresenter: Symbol.for('IRaceRegistrationsPresenter'),
|
||||
DriverRegistrationStatusPresenter: Symbol.for('IDriverRegistrationStatusPresenter'),
|
||||
|
||||
// Presenters - Sponsors
|
||||
SponsorDashboardPresenter: Symbol.for('ISponsorDashboardPresenter'),
|
||||
SponsorSponsorshipsPresenter: Symbol.for('ISponsorSponsorshipsPresenter'),
|
||||
PendingSponsorshipRequestsPresenter: Symbol.for('IPendingSponsorshipRequestsPresenter'),
|
||||
EntitySponsorshipPricingPresenter: Symbol.for('IEntitySponsorshipPricingPresenter'),
|
||||
LeagueSchedulePreviewPresenter: Symbol.for('ILeagueSchedulePreviewPresenter'),
|
||||
} as const;
|
||||
|
||||
export type DITokens = typeof DI_TOKENS;
|
||||
@@ -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';
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
38
apps/website/lib/presenters/AllTeamsPresenter.ts
Normal file
38
apps/website/lib/presenters/AllTeamsPresenter.ts
Normal 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;
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
48
apps/website/lib/presenters/DriverTeamPresenter.ts
Normal file
48
apps/website/lib/presenters/DriverTeamPresenter.ts
Normal 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;
|
||||
}
|
||||
}
|
||||
81
apps/website/lib/presenters/DriversLeaderboardPresenter.ts
Normal file
81
apps/website/lib/presenters/DriversLeaderboardPresenter.ts
Normal 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;
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
119
apps/website/lib/presenters/LeagueFullConfigPresenter.ts
Normal file
119
apps/website/lib/presenters/LeagueFullConfigPresenter.ts
Normal 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' };
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
149
apps/website/lib/presenters/LeagueScoringConfigPresenter.ts
Normal file
149
apps/website/lib/presenters/LeagueScoringConfigPresenter.ts
Normal 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';
|
||||
}
|
||||
}
|
||||
25
apps/website/lib/presenters/LeagueScoringPresetsPresenter.ts
Normal file
25
apps/website/lib/presenters/LeagueScoringPresetsPresenter.ts
Normal 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;
|
||||
}
|
||||
}
|
||||
38
apps/website/lib/presenters/LeagueStandingsPresenter.ts
Normal file
38
apps/website/lib/presenters/LeagueStandingsPresenter.ts
Normal 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;
|
||||
}
|
||||
}
|
||||
42
apps/website/lib/presenters/LeagueStatsPresenter.ts
Normal file
42
apps/website/lib/presenters/LeagueStatsPresenter.ts
Normal 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;
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
60
apps/website/lib/presenters/RacePenaltiesPresenter.ts
Normal file
60
apps/website/lib/presenters/RacePenaltiesPresenter.ts
Normal 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;
|
||||
}
|
||||
}
|
||||
59
apps/website/lib/presenters/RaceProtestsPresenter.ts
Normal file
59
apps/website/lib/presenters/RaceProtestsPresenter.ts
Normal 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;
|
||||
}
|
||||
}
|
||||
24
apps/website/lib/presenters/RaceRegistrationsPresenter.ts
Normal file
24
apps/website/lib/presenters/RaceRegistrationsPresenter.ts
Normal 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;
|
||||
}
|
||||
}
|
||||
49
apps/website/lib/presenters/RaceWithSOFPresenter.ts
Normal file
49
apps/website/lib/presenters/RaceWithSOFPresenter.ts
Normal 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;
|
||||
}
|
||||
}
|
||||
64
apps/website/lib/presenters/RacesPagePresenter.ts
Normal file
64
apps/website/lib/presenters/RacesPagePresenter.ts
Normal 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;
|
||||
}
|
||||
}
|
||||
14
apps/website/lib/presenters/SponsorDashboardPresenter.ts
Normal file
14
apps/website/lib/presenters/SponsorDashboardPresenter.ts
Normal 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;
|
||||
}
|
||||
}
|
||||
14
apps/website/lib/presenters/SponsorSponsorshipsPresenter.ts
Normal file
14
apps/website/lib/presenters/SponsorSponsorshipsPresenter.ts
Normal 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;
|
||||
}
|
||||
}
|
||||
48
apps/website/lib/presenters/TeamDetailsPresenter.ts
Normal file
48
apps/website/lib/presenters/TeamDetailsPresenter.ts
Normal 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;
|
||||
}
|
||||
}
|
||||
43
apps/website/lib/presenters/TeamJoinRequestsPresenter.ts
Normal file
43
apps/website/lib/presenters/TeamJoinRequestsPresenter.ts
Normal 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;
|
||||
}
|
||||
}
|
||||
46
apps/website/lib/presenters/TeamMembersPresenter.ts
Normal file
46
apps/website/lib/presenters/TeamMembersPresenter.ts
Normal 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;
|
||||
}
|
||||
}
|
||||
42
apps/website/lib/presenters/TeamsLeaderboardPresenter.ts
Normal file
42
apps/website/lib/presenters/TeamsLeaderboardPresenter.ts
Normal 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,
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
22
packages/racing/application/presenters/IAllTeamsPresenter.ts
Normal file
22
packages/racing/application/presenters/IAllTeamsPresenter.ts
Normal 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;
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
@@ -0,0 +1,5 @@
|
||||
import type { GetEntitySponsorshipPricingResultDTO } from '../use-cases/GetEntitySponsorshipPricingQuery';
|
||||
|
||||
export interface IEntitySponsorshipPricingPresenter {
|
||||
present(data: GetEntitySponsorshipPricingResultDTO | null): void;
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
@@ -0,0 +1,5 @@
|
||||
import type { LeagueSchedulePreviewDTO } from '../dto/LeagueScheduleDTO';
|
||||
|
||||
export interface ILeagueSchedulePreviewPresenter {
|
||||
present(data: LeagueSchedulePreviewDTO): void;
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
@@ -0,0 +1,10 @@
|
||||
import type { LeagueScoringPresetDTO } from '../ports/LeagueScoringPresetProvider';
|
||||
|
||||
export interface LeagueScoringPresetsViewModel {
|
||||
presets: LeagueScoringPresetDTO[];
|
||||
totalCount: number;
|
||||
}
|
||||
|
||||
export interface ILeagueScoringPresetsPresenter {
|
||||
present(presets: LeagueScoringPresetDTO[]): LeagueScoringPresetsViewModel;
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
@@ -0,0 +1,5 @@
|
||||
import type { GetPendingSponsorshipRequestsResultDTO } from '../use-cases/GetPendingSponsorshipRequestsQuery';
|
||||
|
||||
export interface IPendingSponsorshipRequestsPresenter {
|
||||
present(data: GetPendingSponsorshipRequestsResultDTO): void;
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
export interface RaceRegistrationsViewModel {
|
||||
registeredDriverIds: string[];
|
||||
count: number;
|
||||
}
|
||||
|
||||
export interface IRaceRegistrationsPresenter {
|
||||
present(registeredDriverIds: string[]): RaceRegistrationsViewModel;
|
||||
getViewModel(): RaceRegistrationsViewModel;
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
@@ -0,0 +1,5 @@
|
||||
import type { SponsoredLeagueDTO, SponsorDashboardDTO } from '../use-cases/GetSponsorDashboardQuery';
|
||||
|
||||
export interface ISponsorDashboardPresenter {
|
||||
present(data: SponsorDashboardDTO | null): void;
|
||||
}
|
||||
@@ -0,0 +1,5 @@
|
||||
import type { SponsorSponsorshipsDTO } from '../use-cases/GetSponsorSponsorshipsQuery';
|
||||
|
||||
export interface ISponsorSponsorshipsPresenter {
|
||||
present(data: SponsorSponsorshipsDTO | null): void;
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
@@ -3,23 +3,14 @@ import type { ILeagueMembershipRepository } from '../../domain/repositories/ILea
|
||||
import type { ISeasonRepository } from '../../domain/repositories/ISeasonRepository';
|
||||
import type { ILeagueScoringConfigRepository } from '../../domain/repositories/ILeagueScoringConfigRepository';
|
||||
import type { IGameRepository } from '../../domain/repositories/IGameRepository';
|
||||
import type {
|
||||
LeagueScoringPresetProvider,
|
||||
LeagueScoringPresetDTO,
|
||||
} from '../ports/LeagueScoringPresetProvider';
|
||||
import type {
|
||||
LeagueSummaryDTO,
|
||||
LeagueSummaryScoringDTO,
|
||||
} from '../dto/LeagueSummaryDTO';
|
||||
import type { LeagueScoringPresetProvider } from '../ports/LeagueScoringPresetProvider';
|
||||
import type { IAllLeaguesWithCapacityAndScoringPresenter, LeagueEnrichedData } from '../presenters/IAllLeaguesWithCapacityAndScoringPresenter';
|
||||
|
||||
/**
|
||||
* Combined capacity + scoring summary query for leagues.
|
||||
*
|
||||
* Extends the behavior of GetAllLeaguesWithCapacityQuery by including
|
||||
* scoring preset and game summaries when an active season and
|
||||
* LeagueScoringConfig are available.
|
||||
* Use Case for retrieving all leagues with capacity and scoring information.
|
||||
* Orchestrates domain logic and delegates presentation to the presenter.
|
||||
*/
|
||||
export class GetAllLeaguesWithCapacityAndScoringQuery {
|
||||
export class GetAllLeaguesWithCapacityAndScoringUseCase {
|
||||
constructor(
|
||||
private readonly leagueRepository: ILeagueRepository,
|
||||
private readonly leagueMembershipRepository: ILeagueMembershipRepository,
|
||||
@@ -27,17 +18,16 @@ export class GetAllLeaguesWithCapacityAndScoringQuery {
|
||||
private readonly leagueScoringConfigRepository: ILeagueScoringConfigRepository,
|
||||
private readonly gameRepository: IGameRepository,
|
||||
private readonly presetProvider: LeagueScoringPresetProvider,
|
||||
public readonly presenter: IAllLeaguesWithCapacityAndScoringPresenter,
|
||||
) {}
|
||||
|
||||
async execute(): Promise<LeagueSummaryDTO[]> {
|
||||
async execute(): Promise<void> {
|
||||
const leagues = await this.leagueRepository.findAll();
|
||||
|
||||
const results: LeagueSummaryDTO[] = [];
|
||||
const enrichedLeagues: LeagueEnrichedData[] = [];
|
||||
|
||||
for (const league of leagues) {
|
||||
const members = await this.leagueMembershipRepository.getLeagueMembers(
|
||||
league.id,
|
||||
);
|
||||
const members = await this.leagueMembershipRepository.getLeagueMembers(league.id);
|
||||
|
||||
const usedDriverSlots = members.filter(
|
||||
(m) =>
|
||||
@@ -48,116 +38,36 @@ export class GetAllLeaguesWithCapacityAndScoringQuery {
|
||||
m.role === 'member'),
|
||||
).length;
|
||||
|
||||
const configuredMaxDrivers = league.settings.maxDrivers ?? usedDriverSlots;
|
||||
const safeMaxDrivers = Math.max(configuredMaxDrivers, usedDriverSlots);
|
||||
const seasons = await this.seasonRepository.findByLeagueId(league.id);
|
||||
const activeSeason = seasons && seasons.length > 0
|
||||
? seasons.find((s) => s.status === 'active') ?? seasons[0]
|
||||
: undefined;
|
||||
|
||||
const scoringSummary = await this.buildScoringSummary(league.id);
|
||||
let scoringConfig;
|
||||
let game;
|
||||
let preset;
|
||||
|
||||
const structureSummary = `Solo • ${safeMaxDrivers} drivers`;
|
||||
if (activeSeason) {
|
||||
scoringConfig = await this.leagueScoringConfigRepository.findBySeasonId(activeSeason.id);
|
||||
if (scoringConfig) {
|
||||
game = await this.gameRepository.findById(activeSeason.gameId);
|
||||
const presetId = scoringConfig.scoringPresetId;
|
||||
if (presetId) {
|
||||
preset = this.presetProvider.getPresetById(presetId);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const qualifyingMinutes = 30;
|
||||
const mainRaceMinutes =
|
||||
typeof league.settings.sessionDuration === 'number'
|
||||
? league.settings.sessionDuration
|
||||
: 40;
|
||||
const timingSummary = `${qualifyingMinutes} min Quali • ${mainRaceMinutes} min Race`;
|
||||
|
||||
const dto: LeagueSummaryDTO = {
|
||||
id: league.id,
|
||||
name: league.name,
|
||||
description: league.description,
|
||||
ownerId: league.ownerId,
|
||||
createdAt: league.createdAt,
|
||||
maxDrivers: safeMaxDrivers,
|
||||
enrichedLeagues.push({
|
||||
league,
|
||||
usedDriverSlots,
|
||||
maxTeams: undefined,
|
||||
usedTeamSlots: undefined,
|
||||
structureSummary,
|
||||
scoringPatternSummary: scoringSummary?.scoringPatternSummary,
|
||||
timingSummary,
|
||||
scoring: scoringSummary,
|
||||
};
|
||||
|
||||
results.push(dto);
|
||||
season: activeSeason,
|
||||
scoringConfig,
|
||||
game,
|
||||
preset,
|
||||
});
|
||||
}
|
||||
|
||||
return results;
|
||||
}
|
||||
|
||||
private async buildScoringSummary(
|
||||
leagueId: string,
|
||||
): Promise<LeagueSummaryScoringDTO | undefined> {
|
||||
const seasons = await this.seasonRepository.findByLeagueId(leagueId);
|
||||
if (!seasons || seasons.length === 0) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
const activeSeason =
|
||||
seasons.find((s) => s.status === 'active') ?? seasons[0];
|
||||
|
||||
const scoringConfig =
|
||||
await this.leagueScoringConfigRepository.findBySeasonId(activeSeason.id);
|
||||
if (!scoringConfig) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
const game = await this.gameRepository.findById(activeSeason.gameId);
|
||||
if (!game) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
const presetId = scoringConfig.scoringPresetId;
|
||||
let preset: LeagueScoringPresetDTO | undefined;
|
||||
if (presetId) {
|
||||
preset = this.presetProvider.getPresetById(presetId);
|
||||
}
|
||||
|
||||
const dropPolicySummary =
|
||||
preset?.dropPolicySummary ?? this.deriveDropPolicySummary(scoringConfig);
|
||||
const primaryChampionshipType =
|
||||
preset?.primaryChampionshipType ??
|
||||
(scoringConfig.championships[0]?.type ?? 'driver');
|
||||
|
||||
const scoringPresetName = preset?.name ?? 'Custom';
|
||||
const scoringPatternSummary = `${scoringPresetName} • ${dropPolicySummary}`;
|
||||
|
||||
return {
|
||||
gameId: game.id,
|
||||
gameName: game.name,
|
||||
primaryChampionshipType,
|
||||
scoringPresetId: presetId ?? 'custom',
|
||||
scoringPresetName,
|
||||
dropPolicySummary,
|
||||
scoringPatternSummary,
|
||||
};
|
||||
}
|
||||
|
||||
private deriveDropPolicySummary(config: {
|
||||
championships: Array<{
|
||||
dropScorePolicy: { strategy: string; count?: number; dropCount?: number };
|
||||
}>;
|
||||
}): string {
|
||||
const championship = config.championships[0];
|
||||
if (!championship) {
|
||||
return 'All results count';
|
||||
}
|
||||
|
||||
const policy = championship.dropScorePolicy;
|
||||
if (!policy || policy.strategy === 'none') {
|
||||
return 'All results count';
|
||||
}
|
||||
|
||||
if (policy.strategy === 'bestNResults' && typeof policy.count === 'number') {
|
||||
return `Best ${policy.count} results count`;
|
||||
}
|
||||
|
||||
if (
|
||||
policy.strategy === 'dropWorstN' &&
|
||||
typeof policy.dropCount === 'number'
|
||||
) {
|
||||
return `Worst ${policy.dropCount} results are dropped`;
|
||||
}
|
||||
|
||||
return 'Custom drop score rules';
|
||||
this.presenter.present(enrichedLeagues);
|
||||
}
|
||||
}
|
||||
@@ -1,17 +1,22 @@
|
||||
import type { ILeagueRepository } from '../../domain/repositories/ILeagueRepository';
|
||||
import type { ILeagueMembershipRepository } from '../../domain/repositories/ILeagueMembershipRepository';
|
||||
import type { LeagueDTO } from '../dto/LeagueDTO';
|
||||
import type { IAllLeaguesWithCapacityPresenter } from '../presenters/IAllLeaguesWithCapacityPresenter';
|
||||
|
||||
export class GetAllLeaguesWithCapacityQuery {
|
||||
/**
|
||||
* Use Case for retrieving all leagues with capacity information.
|
||||
* Orchestrates domain logic and delegates presentation to the presenter.
|
||||
*/
|
||||
export class GetAllLeaguesWithCapacityUseCase {
|
||||
constructor(
|
||||
private readonly leagueRepository: ILeagueRepository,
|
||||
private readonly leagueMembershipRepository: ILeagueMembershipRepository,
|
||||
public readonly presenter: IAllLeaguesWithCapacityPresenter,
|
||||
) {}
|
||||
|
||||
async execute(): Promise<LeagueDTO[]> {
|
||||
async execute(): Promise<void> {
|
||||
const leagues = await this.leagueRepository.findAll();
|
||||
|
||||
const results: LeagueDTO[] = [];
|
||||
const memberCounts = new Map<string, number>();
|
||||
|
||||
for (const league of leagues) {
|
||||
const members = await this.leagueMembershipRepository.getLeagueMembers(league.id);
|
||||
@@ -25,34 +30,9 @@ export class GetAllLeaguesWithCapacityQuery {
|
||||
m.role === 'member'),
|
||||
).length;
|
||||
|
||||
// Ensure we never expose an impossible state like 26/24:
|
||||
// clamp maxDrivers to at least usedSlots at the application boundary.
|
||||
const configuredMax = league.settings.maxDrivers ?? usedSlots;
|
||||
const safeMaxDrivers = Math.max(configuredMax, usedSlots);
|
||||
|
||||
const dto: LeagueDTO = {
|
||||
id: league.id,
|
||||
name: league.name,
|
||||
description: league.description,
|
||||
ownerId: league.ownerId,
|
||||
settings: {
|
||||
...league.settings,
|
||||
maxDrivers: safeMaxDrivers,
|
||||
},
|
||||
createdAt: league.createdAt.toISOString(),
|
||||
socialLinks: league.socialLinks
|
||||
? {
|
||||
discordUrl: league.socialLinks.discordUrl,
|
||||
youtubeUrl: league.socialLinks.youtubeUrl,
|
||||
websiteUrl: league.socialLinks.websiteUrl,
|
||||
}
|
||||
: undefined,
|
||||
usedSlots,
|
||||
};
|
||||
|
||||
results.push(dto);
|
||||
memberCounts.set(league.id, usedSlots);
|
||||
}
|
||||
|
||||
return results;
|
||||
this.presenter.present(leagues, memberCounts);
|
||||
}
|
||||
}
|
||||
@@ -1,13 +1,32 @@
|
||||
import type { ITeamRepository } from '../../domain/repositories/ITeamRepository';
|
||||
import type { GetAllTeamsQueryResultDTO } from '../dto/TeamCommandAndQueryDTO';
|
||||
import type { ITeamMembershipRepository } from '../../domain/repositories/ITeamMembershipRepository';
|
||||
import type { IAllTeamsPresenter } from '../presenters/IAllTeamsPresenter';
|
||||
|
||||
export class GetAllTeamsQuery {
|
||||
/**
|
||||
* Use Case for retrieving all teams.
|
||||
* Orchestrates domain logic and delegates presentation to the presenter.
|
||||
*/
|
||||
export class GetAllTeamsUseCase {
|
||||
constructor(
|
||||
private readonly teamRepository: ITeamRepository,
|
||||
private readonly teamMembershipRepository: ITeamMembershipRepository,
|
||||
public readonly presenter: IAllTeamsPresenter,
|
||||
) {}
|
||||
|
||||
async execute(): Promise<GetAllTeamsQueryResultDTO> {
|
||||
async execute(): Promise<void> {
|
||||
const teams = await this.teamRepository.findAll();
|
||||
return teams;
|
||||
|
||||
// Enrich teams with member counts
|
||||
const enrichedTeams = await Promise.all(
|
||||
teams.map(async (team) => {
|
||||
const memberships = await this.teamMembershipRepository.findByTeamId(team.id);
|
||||
return {
|
||||
...team,
|
||||
memberCount: memberships.length,
|
||||
};
|
||||
})
|
||||
);
|
||||
|
||||
this.presenter.present(enrichedTeams as any);
|
||||
}
|
||||
}
|
||||
@@ -1,29 +1,30 @@
|
||||
import type { ITeamRepository } from '../../domain/repositories/ITeamRepository';
|
||||
import type { ITeamMembershipRepository } from '../../domain/repositories/ITeamMembershipRepository';
|
||||
import type {
|
||||
GetDriverTeamQueryParamsDTO,
|
||||
GetDriverTeamQueryResultDTO,
|
||||
} from '../dto/TeamCommandAndQueryDTO';
|
||||
import type { IDriverTeamPresenter } from '../presenters/IDriverTeamPresenter';
|
||||
|
||||
export class GetDriverTeamQuery {
|
||||
/**
|
||||
* Use Case for retrieving a driver's team.
|
||||
* Orchestrates domain logic and delegates presentation to the presenter.
|
||||
*/
|
||||
export class GetDriverTeamUseCase {
|
||||
constructor(
|
||||
private readonly teamRepository: ITeamRepository,
|
||||
private readonly membershipRepository: ITeamMembershipRepository,
|
||||
public readonly presenter: IDriverTeamPresenter,
|
||||
) {}
|
||||
|
||||
async execute(params: GetDriverTeamQueryParamsDTO): Promise<GetDriverTeamQueryResultDTO | null> {
|
||||
const { driverId } = params;
|
||||
|
||||
async execute(driverId: string): Promise<boolean> {
|
||||
const membership = await this.membershipRepository.getActiveMembershipForDriver(driverId);
|
||||
if (!membership) {
|
||||
return null;
|
||||
return false;
|
||||
}
|
||||
|
||||
const team = await this.teamRepository.findById(membership.teamId);
|
||||
if (!team) {
|
||||
return null;
|
||||
return false;
|
||||
}
|
||||
|
||||
return { team, membership };
|
||||
this.presenter.present(team, membership, driverId);
|
||||
return true;
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -1,6 +1,6 @@
|
||||
/**
|
||||
* Query: GetEntitySponsorshipPricingQuery
|
||||
*
|
||||
* Application Use Case: GetEntitySponsorshipPricingUseCase
|
||||
*
|
||||
* Retrieves sponsorship pricing configuration for any entity.
|
||||
* Used by sponsors to see available slots and prices.
|
||||
*/
|
||||
@@ -10,6 +10,7 @@ import type { ISponsorshipRequestRepository } from '../../domain/repositories/IS
|
||||
import type { ISeasonSponsorshipRepository } from '../../domain/repositories/ISeasonSponsorshipRepository';
|
||||
import type { SponsorableEntityType } from '../../domain/entities/SponsorshipRequest';
|
||||
import type { SponsorshipTier } from '../../domain/entities/SeasonSponsorship';
|
||||
import type { IEntitySponsorshipPricingPresenter } from '../presenters/IEntitySponsorshipPricingPresenter';
|
||||
|
||||
export interface GetEntitySponsorshipPricingDTO {
|
||||
entityType: SponsorableEntityType;
|
||||
@@ -37,18 +38,20 @@ export interface GetEntitySponsorshipPricingResultDTO {
|
||||
secondarySlot?: SponsorshipSlotDTO;
|
||||
}
|
||||
|
||||
export class GetEntitySponsorshipPricingQuery {
|
||||
export class GetEntitySponsorshipPricingUseCase {
|
||||
constructor(
|
||||
private readonly sponsorshipPricingRepo: ISponsorshipPricingRepository,
|
||||
private readonly sponsorshipRequestRepo: ISponsorshipRequestRepository,
|
||||
private readonly seasonSponsorshipRepo: ISeasonSponsorshipRepository,
|
||||
private readonly presenter: IEntitySponsorshipPricingPresenter,
|
||||
) {}
|
||||
|
||||
async execute(dto: GetEntitySponsorshipPricingDTO): Promise<GetEntitySponsorshipPricingResultDTO | null> {
|
||||
async execute(dto: GetEntitySponsorshipPricingDTO): Promise<void> {
|
||||
const pricing = await this.sponsorshipPricingRepo.findByEntity(dto.entityType, dto.entityId);
|
||||
|
||||
if (!pricing) {
|
||||
return null;
|
||||
this.presenter.present(null);
|
||||
return;
|
||||
}
|
||||
|
||||
// Count pending requests by tier
|
||||
@@ -107,6 +110,6 @@ export class GetEntitySponsorshipPricingQuery {
|
||||
};
|
||||
}
|
||||
|
||||
return result;
|
||||
this.presenter.present(result);
|
||||
}
|
||||
}
|
||||
@@ -2,26 +2,31 @@ import type { IStandingRepository } from '../../domain/repositories/IStandingRep
|
||||
import type { IResultRepository } from '../../domain/repositories/IResultRepository';
|
||||
import type { IPenaltyRepository } from '../../domain/repositories/IPenaltyRepository';
|
||||
import type { IRaceRepository } from '../../domain/repositories/IRaceRepository';
|
||||
import type { LeagueDriverSeasonStatsDTO } from '../dto/LeagueDriverSeasonStatsDTO';
|
||||
import type { ILeagueDriverSeasonStatsPresenter } from '../presenters/ILeagueDriverSeasonStatsPresenter';
|
||||
|
||||
export interface DriverRatingPort {
|
||||
getRating(driverId: string): { rating: number | null; ratingChange: number | null };
|
||||
}
|
||||
|
||||
export interface GetLeagueDriverSeasonStatsQueryParamsDTO {
|
||||
export interface GetLeagueDriverSeasonStatsUseCaseParams {
|
||||
leagueId: string;
|
||||
}
|
||||
|
||||
export class GetLeagueDriverSeasonStatsQuery {
|
||||
/**
|
||||
* Use Case for retrieving league driver season statistics.
|
||||
* Orchestrates domain logic and delegates presentation to the presenter.
|
||||
*/
|
||||
export class GetLeagueDriverSeasonStatsUseCase {
|
||||
constructor(
|
||||
private readonly standingRepository: IStandingRepository,
|
||||
private readonly resultRepository: IResultRepository,
|
||||
private readonly penaltyRepository: IPenaltyRepository,
|
||||
private readonly raceRepository: IRaceRepository,
|
||||
private readonly driverRatingPort: DriverRatingPort,
|
||||
public readonly presenter: ILeagueDriverSeasonStatsPresenter,
|
||||
) {}
|
||||
|
||||
async execute(params: GetLeagueDriverSeasonStatsQueryParamsDTO): Promise<LeagueDriverSeasonStatsDTO[]> {
|
||||
async execute(params: GetLeagueDriverSeasonStatsUseCaseParams): Promise<void> {
|
||||
const { leagueId } = params;
|
||||
|
||||
// Get standings and races for the league
|
||||
@@ -53,59 +58,26 @@ export class GetLeagueDriverSeasonStatsQuery {
|
||||
penaltiesByDriver.set(p.driverId, current);
|
||||
}
|
||||
|
||||
// Build basic stats per driver from standings
|
||||
const statsByDriver = new Map<string, LeagueDriverSeasonStatsDTO>();
|
||||
|
||||
// Collect driver ratings
|
||||
const driverRatings = new Map<string, { rating: number | null; ratingChange: number | null }>();
|
||||
for (const standing of standings) {
|
||||
const penalty = penaltiesByDriver.get(standing.driverId) ?? { baseDelta: 0, bonusDelta: 0 };
|
||||
const totalPenaltyPoints = penalty.baseDelta;
|
||||
const bonusPoints = penalty.bonusDelta;
|
||||
|
||||
const racesCompleted = standing.racesCompleted;
|
||||
const pointsPerRace = racesCompleted > 0 ? standing.points / racesCompleted : 0;
|
||||
|
||||
const ratingInfo = this.driverRatingPort.getRating(standing.driverId);
|
||||
|
||||
const dto: LeagueDriverSeasonStatsDTO = {
|
||||
leagueId,
|
||||
driverId: standing.driverId,
|
||||
position: standing.position,
|
||||
driverName: '',
|
||||
teamId: undefined,
|
||||
teamName: undefined,
|
||||
totalPoints: standing.points + totalPenaltyPoints + bonusPoints,
|
||||
basePoints: standing.points,
|
||||
penaltyPoints: Math.abs(totalPenaltyPoints),
|
||||
bonusPoints,
|
||||
pointsPerRace,
|
||||
racesStarted: racesCompleted,
|
||||
racesFinished: racesCompleted,
|
||||
dnfs: 0,
|
||||
noShows: 0,
|
||||
avgFinish: null,
|
||||
rating: ratingInfo.rating,
|
||||
ratingChange: ratingInfo.ratingChange,
|
||||
};
|
||||
|
||||
statsByDriver.set(standing.driverId, dto);
|
||||
driverRatings.set(standing.driverId, ratingInfo);
|
||||
}
|
||||
|
||||
// Enhance stats with basic finish-position-based avgFinish from results
|
||||
for (const [driverId, dto] of statsByDriver.entries()) {
|
||||
const driverResults = await this.resultRepository.findByDriverIdAndLeagueId(driverId, leagueId);
|
||||
if (driverResults.length > 0) {
|
||||
const totalPositions = driverResults.reduce((sum, r) => sum + r.position, 0);
|
||||
const avgFinish = totalPositions / driverResults.length;
|
||||
dto.avgFinish = Number.isFinite(avgFinish) ? Number(avgFinish.toFixed(2)) : null;
|
||||
dto.racesStarted = driverResults.length;
|
||||
dto.racesFinished = driverResults.length;
|
||||
}
|
||||
statsByDriver.set(driverId, dto);
|
||||
// Collect driver results
|
||||
const driverResults = new Map<string, Array<{ position: number }>>();
|
||||
for (const standing of standings) {
|
||||
const results = await this.resultRepository.findByDriverIdAndLeagueId(standing.driverId, leagueId);
|
||||
driverResults.set(standing.driverId, results);
|
||||
}
|
||||
|
||||
// Ensure ordering by position
|
||||
const result = Array.from(statsByDriver.values()).sort((a, b) => a.position - b.position);
|
||||
|
||||
return result;
|
||||
this.presenter.present(
|
||||
leagueId,
|
||||
standings,
|
||||
penaltiesByDriver,
|
||||
driverResults,
|
||||
driverRatings
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -2,153 +2,52 @@ import type { ILeagueRepository } from '../../domain/repositories/ILeagueReposit
|
||||
import type { ISeasonRepository } from '../../domain/repositories/ISeasonRepository';
|
||||
import type { ILeagueScoringConfigRepository } from '../../domain/repositories/ILeagueScoringConfigRepository';
|
||||
import type { IGameRepository } from '../../domain/repositories/IGameRepository';
|
||||
import type { ChampionshipConfig } from '../../domain/value-objects/ChampionshipConfig';
|
||||
import type { DropScorePolicy } from '../../domain/value-objects/DropScorePolicy';
|
||||
import type {
|
||||
LeagueConfigFormModel,
|
||||
LeagueDropPolicyFormDTO,
|
||||
} from '../dto/LeagueConfigFormDTO';
|
||||
import type { ILeagueFullConfigPresenter, LeagueFullConfigData } from '../presenters/ILeagueFullConfigPresenter';
|
||||
|
||||
/**
|
||||
* Query returning a unified LeagueConfigFormModel for a given league.
|
||||
*
|
||||
* First iteration focuses on:
|
||||
* - Basics derived from League
|
||||
* - Simple solo structure derived from League.settings.maxDrivers
|
||||
* - Championships flags with driver enabled and others disabled
|
||||
* - Scoring pattern id taken from LeagueScoringConfig.scoringPresetId
|
||||
* - Drop policy inferred from the primary championship configuration
|
||||
* Use Case for retrieving a league's full configuration.
|
||||
* Orchestrates domain logic and delegates presentation to the presenter.
|
||||
*/
|
||||
export class GetLeagueFullConfigQuery {
|
||||
export class GetLeagueFullConfigUseCase {
|
||||
constructor(
|
||||
private readonly leagueRepository: ILeagueRepository,
|
||||
private readonly seasonRepository: ISeasonRepository,
|
||||
private readonly leagueScoringConfigRepository: ILeagueScoringConfigRepository,
|
||||
private readonly gameRepository: IGameRepository,
|
||||
public readonly presenter: ILeagueFullConfigPresenter,
|
||||
) {}
|
||||
|
||||
async execute(params: { leagueId: string }): Promise<LeagueConfigFormModel | null> {
|
||||
async execute(params: { leagueId: string }): Promise<void> {
|
||||
const { leagueId } = params;
|
||||
|
||||
const league = await this.leagueRepository.findById(leagueId);
|
||||
if (!league) {
|
||||
return null;
|
||||
throw new Error(`League ${leagueId} not found`);
|
||||
}
|
||||
|
||||
const seasons = await this.seasonRepository.findByLeagueId(leagueId);
|
||||
const activeSeason =
|
||||
seasons && seasons.length > 0
|
||||
? seasons.find((s) => s.status === 'active') ?? seasons[0]
|
||||
: null;
|
||||
|
||||
const scoringConfig = activeSeason
|
||||
? await this.leagueScoringConfigRepository.findBySeasonId(activeSeason.id)
|
||||
: null;
|
||||
|
||||
const game =
|
||||
activeSeason && activeSeason.gameId
|
||||
? await this.gameRepository.findById(activeSeason.gameId)
|
||||
: null;
|
||||
|
||||
const patternId = scoringConfig?.scoringPresetId;
|
||||
|
||||
const primaryChampionship: ChampionshipConfig | undefined =
|
||||
scoringConfig && scoringConfig.championships && scoringConfig.championships.length > 0
|
||||
? scoringConfig.championships[0]
|
||||
: undefined;
|
||||
|
||||
const dropPolicy: DropScorePolicy | undefined =
|
||||
primaryChampionship?.dropScorePolicy ?? undefined;
|
||||
let scoringConfig;
|
||||
let game;
|
||||
|
||||
const dropPolicyForm: LeagueDropPolicyFormDTO = this.mapDropPolicy(dropPolicy);
|
||||
|
||||
const defaultQualifyingMinutes = 30;
|
||||
const defaultMainRaceMinutes = 40;
|
||||
const mainRaceMinutes =
|
||||
typeof league.settings.sessionDuration === 'number'
|
||||
? league.settings.sessionDuration
|
||||
: defaultMainRaceMinutes;
|
||||
const qualifyingMinutes = defaultQualifyingMinutes;
|
||||
|
||||
const roundsPlanned = 8;
|
||||
|
||||
let sessionCount = 2;
|
||||
if (
|
||||
primaryChampionship &&
|
||||
Array.isArray((primaryChampionship as any).sessionTypes) &&
|
||||
(primaryChampionship as any).sessionTypes.length > 0
|
||||
) {
|
||||
sessionCount = (primaryChampionship as any).sessionTypes.length;
|
||||
if (activeSeason) {
|
||||
scoringConfig = await this.leagueScoringConfigRepository.findBySeasonId(activeSeason.id);
|
||||
if (activeSeason.gameId) {
|
||||
game = await this.gameRepository.findById(activeSeason.gameId);
|
||||
}
|
||||
}
|
||||
|
||||
const practiceMinutes = 20;
|
||||
const sprintRaceMinutes = patternId === 'sprint-main-driver' ? 20 : undefined;
|
||||
|
||||
const form: LeagueConfigFormModel = {
|
||||
leagueId: league.id,
|
||||
basics: {
|
||||
name: league.name,
|
||||
description: league.description,
|
||||
visibility: 'public', // current domain model does not track visibility; default to public for now
|
||||
gameId: game?.id ?? 'iracing',
|
||||
},
|
||||
structure: {
|
||||
// First slice: treat everything as solo structure based on maxDrivers
|
||||
mode: 'solo',
|
||||
maxDrivers: league.settings.maxDrivers ?? 32,
|
||||
maxTeams: undefined,
|
||||
driversPerTeam: undefined,
|
||||
multiClassEnabled: false,
|
||||
},
|
||||
championships: {
|
||||
enableDriverChampionship: true,
|
||||
enableTeamChampionship: false,
|
||||
enableNationsChampionship: false,
|
||||
enableTrophyChampionship: false,
|
||||
},
|
||||
scoring: {
|
||||
patternId: patternId ?? undefined,
|
||||
customScoringEnabled: !patternId,
|
||||
},
|
||||
dropPolicy: dropPolicyForm,
|
||||
timings: {
|
||||
practiceMinutes,
|
||||
qualifyingMinutes,
|
||||
sprintRaceMinutes,
|
||||
mainRaceMinutes,
|
||||
sessionCount,
|
||||
roundsPlanned,
|
||||
},
|
||||
stewarding: {
|
||||
decisionMode: 'admin_only',
|
||||
requireDefense: true,
|
||||
defenseTimeLimit: 48,
|
||||
voteTimeLimit: 72,
|
||||
protestDeadlineHours: 72,
|
||||
stewardingClosesHours: 168,
|
||||
notifyAccusedOnProtest: true,
|
||||
notifyOnVoteRequired: true,
|
||||
},
|
||||
const data: LeagueFullConfigData = {
|
||||
league,
|
||||
activeSeason,
|
||||
scoringConfig,
|
||||
game,
|
||||
};
|
||||
|
||||
return form;
|
||||
}
|
||||
|
||||
private mapDropPolicy(policy: DropScorePolicy | undefined): LeagueDropPolicyFormDTO {
|
||||
if (!policy || policy.strategy === 'none') {
|
||||
return { strategy: 'none' };
|
||||
}
|
||||
|
||||
if (policy.strategy === 'bestNResults') {
|
||||
const n = typeof policy.count === 'number' ? policy.count : undefined;
|
||||
return n !== undefined ? { strategy: 'bestNResults', n } : { strategy: 'none' };
|
||||
}
|
||||
|
||||
if (policy.strategy === 'dropWorstN') {
|
||||
const n = typeof policy.dropCount === 'number' ? policy.dropCount : undefined;
|
||||
return n !== undefined ? { strategy: 'dropWorstN', n } : { strategy: 'none' };
|
||||
}
|
||||
|
||||
return { strategy: 'none' };
|
||||
this.presenter.present(data);
|
||||
}
|
||||
}
|
||||
@@ -2,41 +2,34 @@ import type { ILeagueRepository } from '../../domain/repositories/ILeagueReposit
|
||||
import type { ISeasonRepository } from '../../domain/repositories/ISeasonRepository';
|
||||
import type { ILeagueScoringConfigRepository } from '../../domain/repositories/ILeagueScoringConfigRepository';
|
||||
import type { IGameRepository } from '../../domain/repositories/IGameRepository';
|
||||
import type { LeagueScoringConfigDTO } from '../dto/LeagueScoringConfigDTO';
|
||||
import type { LeagueScoringChampionshipDTO } from '../dto/LeagueScoringConfigDTO';
|
||||
import type {
|
||||
LeagueScoringPresetProvider,
|
||||
LeagueScoringPresetDTO,
|
||||
} from '../ports/LeagueScoringPresetProvider';
|
||||
import type { ChampionshipConfig } from '../../domain/value-objects/ChampionshipConfig';
|
||||
import type { PointsTable } from '../../domain/value-objects/PointsTable';
|
||||
import type { BonusRule } from '../../domain/value-objects/BonusRule';
|
||||
import type { LeagueScoringPresetProvider } from '../ports/LeagueScoringPresetProvider';
|
||||
import type { ILeagueScoringConfigPresenter, LeagueScoringConfigData } from '../presenters/ILeagueScoringConfigPresenter';
|
||||
|
||||
/**
|
||||
* Query returning a league's scoring configuration for its active season.
|
||||
*
|
||||
* Designed for the league detail "Scoring" tab.
|
||||
* Use Case for retrieving a league's scoring configuration for its active season.
|
||||
* Orchestrates domain logic and delegates presentation to the presenter.
|
||||
*/
|
||||
export class GetLeagueScoringConfigQuery {
|
||||
export class GetLeagueScoringConfigUseCase {
|
||||
constructor(
|
||||
private readonly leagueRepository: ILeagueRepository,
|
||||
private readonly seasonRepository: ISeasonRepository,
|
||||
private readonly leagueScoringConfigRepository: ILeagueScoringConfigRepository,
|
||||
private readonly gameRepository: IGameRepository,
|
||||
private readonly presetProvider: LeagueScoringPresetProvider,
|
||||
public readonly presenter: ILeagueScoringConfigPresenter,
|
||||
) {}
|
||||
|
||||
async execute(params: { leagueId: string }): Promise<LeagueScoringConfigDTO | null> {
|
||||
async execute(params: { leagueId: string }): Promise<void> {
|
||||
const { leagueId } = params;
|
||||
|
||||
const league = await this.leagueRepository.findById(leagueId);
|
||||
if (!league) {
|
||||
return null;
|
||||
throw new Error(`League ${leagueId} not found`);
|
||||
}
|
||||
|
||||
const seasons = await this.seasonRepository.findByLeagueId(leagueId);
|
||||
if (!seasons || seasons.length === 0) {
|
||||
return null;
|
||||
throw new Error(`No seasons found for league ${leagueId}`);
|
||||
}
|
||||
|
||||
const activeSeason =
|
||||
@@ -45,146 +38,27 @@ export class GetLeagueScoringConfigQuery {
|
||||
const scoringConfig =
|
||||
await this.leagueScoringConfigRepository.findBySeasonId(activeSeason.id);
|
||||
if (!scoringConfig) {
|
||||
return null;
|
||||
throw new Error(`No scoring config found for season ${activeSeason.id}`);
|
||||
}
|
||||
|
||||
const game = await this.gameRepository.findById(activeSeason.gameId);
|
||||
if (!game) {
|
||||
return null;
|
||||
throw new Error(`Game ${activeSeason.gameId} not found`);
|
||||
}
|
||||
|
||||
const presetId = scoringConfig.scoringPresetId;
|
||||
const preset: LeagueScoringPresetDTO | undefined =
|
||||
presetId ? this.presetProvider.getPresetById(presetId) : undefined;
|
||||
const preset = presetId ? this.presetProvider.getPresetById(presetId) : undefined;
|
||||
|
||||
const championships: LeagueScoringChampionshipDTO[] =
|
||||
scoringConfig.championships.map((champ) =>
|
||||
this.mapChampionship(champ),
|
||||
);
|
||||
|
||||
const dropPolicySummary =
|
||||
preset?.dropPolicySummary ??
|
||||
this.deriveDropPolicyDescriptionFromChampionships(
|
||||
scoringConfig.championships,
|
||||
);
|
||||
|
||||
return {
|
||||
const data: LeagueScoringConfigData = {
|
||||
leagueId: league.id,
|
||||
seasonId: activeSeason.id,
|
||||
gameId: game.id,
|
||||
gameName: game.name,
|
||||
scoringPresetId: presetId,
|
||||
scoringPresetName: preset?.name,
|
||||
dropPolicySummary,
|
||||
championships,
|
||||
preset,
|
||||
championships: scoringConfig.championships,
|
||||
};
|
||||
}
|
||||
|
||||
private mapChampionship(championship: ChampionshipConfig): LeagueScoringChampionshipDTO {
|
||||
const sessionTypes = championship.sessionTypes.map((s) => s.toString());
|
||||
const pointsPreview = this.buildPointsPreview(championship.pointsTableBySessionType);
|
||||
const bonusSummary = this.buildBonusSummary(
|
||||
championship.bonusRulesBySessionType ?? {},
|
||||
);
|
||||
const dropPolicyDescription = this.deriveDropPolicyDescription(
|
||||
championship.dropScorePolicy,
|
||||
);
|
||||
|
||||
return {
|
||||
id: championship.id,
|
||||
name: championship.name,
|
||||
type: championship.type,
|
||||
sessionTypes,
|
||||
pointsPreview,
|
||||
bonusSummary,
|
||||
dropPolicyDescription,
|
||||
};
|
||||
}
|
||||
|
||||
private buildPointsPreview(
|
||||
tables: Record<string, any>,
|
||||
): Array<{ sessionType: string; position: number; points: number }> {
|
||||
const preview: Array<{
|
||||
sessionType: string;
|
||||
position: number;
|
||||
points: number;
|
||||
}> = [];
|
||||
|
||||
const maxPositions = 10;
|
||||
|
||||
for (const [sessionType, table] of Object.entries(tables)) {
|
||||
for (let pos = 1; pos <= maxPositions; pos++) {
|
||||
const points = table.getPointsForPosition(pos);
|
||||
if (points && points !== 0) {
|
||||
preview.push({
|
||||
sessionType,
|
||||
position: pos,
|
||||
points,
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return preview;
|
||||
}
|
||||
|
||||
private buildBonusSummary(
|
||||
bonusRulesBySessionType: Record<string, BonusRule[]>,
|
||||
): string[] {
|
||||
const summaries: string[] = [];
|
||||
|
||||
for (const [sessionType, rules] of Object.entries(bonusRulesBySessionType)) {
|
||||
for (const rule of rules) {
|
||||
if (rule.type === 'fastestLap') {
|
||||
const base = `Fastest lap in ${sessionType}`;
|
||||
if (rule.requiresFinishInTopN) {
|
||||
summaries.push(
|
||||
`${base} +${rule.points} points if finishing P${rule.requiresFinishInTopN} or better`,
|
||||
);
|
||||
} else {
|
||||
summaries.push(`${base} +${rule.points} points`);
|
||||
}
|
||||
} else {
|
||||
summaries.push(
|
||||
`${rule.type} bonus in ${sessionType} worth ${rule.points} points`,
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return summaries;
|
||||
}
|
||||
|
||||
private deriveDropPolicyDescriptionFromChampionships(
|
||||
championships: ChampionshipConfig[],
|
||||
): string {
|
||||
const first = championships[0];
|
||||
if (!first) {
|
||||
return 'All results count';
|
||||
}
|
||||
return this.deriveDropPolicyDescription(first.dropScorePolicy);
|
||||
}
|
||||
|
||||
private deriveDropPolicyDescription(policy: {
|
||||
strategy: string;
|
||||
count?: number;
|
||||
dropCount?: number;
|
||||
}): string {
|
||||
if (!policy || policy.strategy === 'none') {
|
||||
return 'All results count';
|
||||
}
|
||||
|
||||
if (policy.strategy === 'bestNResults' && typeof policy.count === 'number') {
|
||||
return `Best ${policy.count} results count towards the championship`;
|
||||
}
|
||||
|
||||
if (
|
||||
policy.strategy === 'dropWorstN' &&
|
||||
typeof policy.dropCount === 'number'
|
||||
) {
|
||||
return `Worst ${policy.dropCount} results are dropped from the championship total`;
|
||||
}
|
||||
|
||||
return 'Custom drop score rules apply';
|
||||
this.presenter.present(data);
|
||||
}
|
||||
}
|
||||
@@ -1,18 +1,22 @@
|
||||
import type { IStandingRepository } from '../../domain/repositories/IStandingRepository';
|
||||
import type { StandingDTO } from '../dto/StandingDTO';
|
||||
import { EntityMappers } from '../mappers/EntityMappers';
|
||||
import type { ILeagueStandingsPresenter } from '../presenters/ILeagueStandingsPresenter';
|
||||
|
||||
export interface GetLeagueStandingsQueryParamsDTO {
|
||||
export interface GetLeagueStandingsUseCaseParams {
|
||||
leagueId: string;
|
||||
}
|
||||
|
||||
export class GetLeagueStandingsQuery {
|
||||
/**
|
||||
* Use Case for retrieving league standings.
|
||||
* Orchestrates domain logic and delegates presentation to the presenter.
|
||||
*/
|
||||
export class GetLeagueStandingsUseCase {
|
||||
constructor(
|
||||
private readonly standingRepository: IStandingRepository,
|
||||
public readonly presenter: ILeagueStandingsPresenter,
|
||||
) {}
|
||||
|
||||
async execute(params: GetLeagueStandingsQueryParamsDTO): Promise<StandingDTO[]> {
|
||||
async execute(params: GetLeagueStandingsUseCaseParams): Promise<void> {
|
||||
const standings = await this.standingRepository.findByLeagueId(params.leagueId);
|
||||
return EntityMappers.toStandingDTOs(standings);
|
||||
this.presenter.present(standings);
|
||||
}
|
||||
}
|
||||
@@ -1,33 +1,26 @@
|
||||
/**
|
||||
* Application Query: GetLeagueStatsQuery
|
||||
*
|
||||
* Returns league statistics including average SOF across completed races.
|
||||
* Use Case for retrieving league statistics.
|
||||
* Orchestrates domain logic and delegates presentation to the presenter.
|
||||
*/
|
||||
|
||||
import type { ILeagueRepository } from '../../domain/repositories/ILeagueRepository';
|
||||
import type { IRaceRepository } from '../../domain/repositories/IRaceRepository';
|
||||
import type { IResultRepository } from '../../domain/repositories/IResultRepository';
|
||||
import type { DriverRatingProvider } from '../ports/DriverRatingProvider';
|
||||
import type { ILeagueStatsPresenter } from '../presenters/ILeagueStatsPresenter';
|
||||
import {
|
||||
AverageStrengthOfFieldCalculator,
|
||||
type StrengthOfFieldCalculator,
|
||||
} from '../../domain/services/StrengthOfFieldCalculator';
|
||||
|
||||
export interface GetLeagueStatsQueryParams {
|
||||
export interface GetLeagueStatsUseCaseParams {
|
||||
leagueId: string;
|
||||
}
|
||||
|
||||
export interface LeagueStatsDTO {
|
||||
leagueId: string;
|
||||
totalRaces: number;
|
||||
completedRaces: number;
|
||||
scheduledRaces: number;
|
||||
averageSOF: number | null;
|
||||
highestSOF: number | null;
|
||||
lowestSOF: number | null;
|
||||
}
|
||||
|
||||
export class GetLeagueStatsQuery {
|
||||
/**
|
||||
* Use Case for retrieving league statistics including average SOF across completed races.
|
||||
*/
|
||||
export class GetLeagueStatsUseCase {
|
||||
private readonly sofCalculator: StrengthOfFieldCalculator;
|
||||
|
||||
constructor(
|
||||
@@ -35,17 +28,18 @@ export class GetLeagueStatsQuery {
|
||||
private readonly raceRepository: IRaceRepository,
|
||||
private readonly resultRepository: IResultRepository,
|
||||
private readonly driverRatingProvider: DriverRatingProvider,
|
||||
public readonly presenter: ILeagueStatsPresenter,
|
||||
sofCalculator?: StrengthOfFieldCalculator,
|
||||
) {
|
||||
this.sofCalculator = sofCalculator ?? new AverageStrengthOfFieldCalculator();
|
||||
}
|
||||
|
||||
async execute(params: GetLeagueStatsQueryParams): Promise<LeagueStatsDTO | null> {
|
||||
async execute(params: GetLeagueStatsUseCaseParams): Promise<void> {
|
||||
const { leagueId } = params;
|
||||
|
||||
const league = await this.leagueRepository.findById(leagueId);
|
||||
if (!league) {
|
||||
return null;
|
||||
throw new Error(`League ${leagueId} not found`);
|
||||
}
|
||||
|
||||
const races = await this.raceRepository.findByLeagueId(leagueId);
|
||||
@@ -78,22 +72,12 @@ export class GetLeagueStatsQuery {
|
||||
}
|
||||
}
|
||||
|
||||
// Calculate aggregate stats
|
||||
const averageSOF = sofValues.length > 0
|
||||
? Math.round(sofValues.reduce((a, b) => a + b, 0) / sofValues.length)
|
||||
: null;
|
||||
|
||||
const highestSOF = sofValues.length > 0 ? Math.max(...sofValues) : null;
|
||||
const lowestSOF = sofValues.length > 0 ? Math.min(...sofValues) : null;
|
||||
|
||||
return {
|
||||
this.presenter.present(
|
||||
leagueId,
|
||||
totalRaces: races.length,
|
||||
completedRaces: completedRaces.length,
|
||||
scheduledRaces: scheduledRaces.length,
|
||||
averageSOF,
|
||||
highestSOF,
|
||||
lowestSOF,
|
||||
};
|
||||
races.length,
|
||||
completedRaces.length,
|
||||
scheduledRaces.length,
|
||||
sofValues
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -1,6 +1,6 @@
|
||||
/**
|
||||
* Query: GetPendingSponsorshipRequestsQuery
|
||||
*
|
||||
* Application Use Case: GetPendingSponsorshipRequestsUseCase
|
||||
*
|
||||
* Retrieves pending sponsorship requests for an entity owner to review.
|
||||
*/
|
||||
|
||||
@@ -8,6 +8,7 @@ import type { ISponsorshipRequestRepository } from '../../domain/repositories/IS
|
||||
import type { ISponsorRepository } from '../../domain/repositories/ISponsorRepository';
|
||||
import type { SponsorableEntityType } from '../../domain/entities/SponsorshipRequest';
|
||||
import type { SponsorshipTier } from '../../domain/entities/SeasonSponsorship';
|
||||
import type { IPendingSponsorshipRequestsPresenter } from '../presenters/IPendingSponsorshipRequestsPresenter';
|
||||
|
||||
export interface GetPendingSponsorshipRequestsDTO {
|
||||
entityType: SponsorableEntityType;
|
||||
@@ -36,13 +37,14 @@ export interface GetPendingSponsorshipRequestsResultDTO {
|
||||
totalCount: number;
|
||||
}
|
||||
|
||||
export class GetPendingSponsorshipRequestsQuery {
|
||||
export class GetPendingSponsorshipRequestsUseCase {
|
||||
constructor(
|
||||
private readonly sponsorshipRequestRepo: ISponsorshipRequestRepository,
|
||||
private readonly sponsorRepo: ISponsorRepository,
|
||||
private readonly presenter: IPendingSponsorshipRequestsPresenter,
|
||||
) {}
|
||||
|
||||
async execute(dto: GetPendingSponsorshipRequestsDTO): Promise<GetPendingSponsorshipRequestsResultDTO> {
|
||||
async execute(dto: GetPendingSponsorshipRequestsDTO): Promise<void> {
|
||||
const requests = await this.sponsorshipRequestRepo.findPendingByEntity(
|
||||
dto.entityType,
|
||||
dto.entityId
|
||||
@@ -72,11 +74,11 @@ export class GetPendingSponsorshipRequestsQuery {
|
||||
// Sort by creation date (newest first)
|
||||
requestDTOs.sort((a, b) => b.createdAt.getTime() - a.createdAt.getTime());
|
||||
|
||||
return {
|
||||
this.presenter.present({
|
||||
entityType: dto.entityType,
|
||||
entityId: dto.entityId,
|
||||
requests: requestDTOs,
|
||||
totalCount: requestDTOs.length,
|
||||
};
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -1,38 +1,22 @@
|
||||
/**
|
||||
* Application Query: GetRacePenaltiesQuery
|
||||
*
|
||||
* Use Case: GetRacePenaltiesUseCase
|
||||
*
|
||||
* Returns all penalties applied for a specific race, with driver details.
|
||||
* Orchestrates domain logic and delegates presentation to the presenter.
|
||||
*/
|
||||
|
||||
import type { IPenaltyRepository } from '../../domain/repositories/IPenaltyRepository';
|
||||
import type { IDriverRepository } from '../../domain/repositories/IDriverRepository';
|
||||
import type { PenaltyType, PenaltyStatus } from '../../domain/entities/Penalty';
|
||||
import type { IRacePenaltiesPresenter } from '../presenters/IRacePenaltiesPresenter';
|
||||
|
||||
export interface RacePenaltyDTO {
|
||||
id: string;
|
||||
raceId: string;
|
||||
driverId: string;
|
||||
driverName: string;
|
||||
type: PenaltyType;
|
||||
value?: number;
|
||||
reason: string;
|
||||
protestId?: string;
|
||||
issuedBy: string;
|
||||
issuedByName: string;
|
||||
status: PenaltyStatus;
|
||||
description: string;
|
||||
issuedAt: string;
|
||||
appliedAt?: string;
|
||||
notes?: string;
|
||||
}
|
||||
|
||||
export class GetRacePenaltiesQuery {
|
||||
export class GetRacePenaltiesUseCase {
|
||||
constructor(
|
||||
private readonly penaltyRepository: IPenaltyRepository,
|
||||
private readonly driverRepository: IDriverRepository,
|
||||
public readonly presenter: IRacePenaltiesPresenter,
|
||||
) {}
|
||||
|
||||
async execute(raceId: string): Promise<RacePenaltyDTO[]> {
|
||||
async execute(raceId: string): Promise<void> {
|
||||
const penalties = await this.penaltyRepository.findByRaceId(raceId);
|
||||
|
||||
// Load all driver details in parallel
|
||||
@@ -53,22 +37,6 @@ export class GetRacePenaltiesQuery {
|
||||
}
|
||||
});
|
||||
|
||||
return penalties.map(penalty => ({
|
||||
id: penalty.id,
|
||||
raceId: penalty.raceId,
|
||||
driverId: penalty.driverId,
|
||||
driverName: driverMap.get(penalty.driverId) || 'Unknown',
|
||||
type: penalty.type,
|
||||
value: penalty.value,
|
||||
reason: penalty.reason,
|
||||
protestId: penalty.protestId,
|
||||
issuedBy: penalty.issuedBy,
|
||||
issuedByName: driverMap.get(penalty.issuedBy) || 'Unknown',
|
||||
status: penalty.status,
|
||||
description: penalty.getDescription(),
|
||||
issuedAt: penalty.issuedAt.toISOString(),
|
||||
appliedAt: penalty.appliedAt?.toISOString(),
|
||||
notes: penalty.notes,
|
||||
}));
|
||||
this.presenter.present(penalties, driverMap);
|
||||
}
|
||||
}
|
||||
@@ -1,38 +1,22 @@
|
||||
/**
|
||||
* Application Query: GetRaceProtestsQuery
|
||||
*
|
||||
* Use Case: GetRaceProtestsUseCase
|
||||
*
|
||||
* Returns all protests filed for a specific race, with driver details.
|
||||
* Orchestrates domain logic and delegates presentation to the presenter.
|
||||
*/
|
||||
|
||||
import type { IProtestRepository } from '../../domain/repositories/IProtestRepository';
|
||||
import type { IDriverRepository } from '../../domain/repositories/IDriverRepository';
|
||||
import type { ProtestStatus, ProtestIncident } from '../../domain/entities/Protest';
|
||||
import type { IRaceProtestsPresenter } from '../presenters/IRaceProtestsPresenter';
|
||||
|
||||
export interface RaceProtestDTO {
|
||||
id: string;
|
||||
raceId: string;
|
||||
protestingDriverId: string;
|
||||
protestingDriverName: string;
|
||||
accusedDriverId: string;
|
||||
accusedDriverName: string;
|
||||
incident: ProtestIncident;
|
||||
comment?: string;
|
||||
proofVideoUrl?: string;
|
||||
status: ProtestStatus;
|
||||
reviewedBy?: string;
|
||||
reviewedByName?: string;
|
||||
decisionNotes?: string;
|
||||
filedAt: string;
|
||||
reviewedAt?: string;
|
||||
}
|
||||
|
||||
export class GetRaceProtestsQuery {
|
||||
export class GetRaceProtestsUseCase {
|
||||
constructor(
|
||||
private readonly protestRepository: IProtestRepository,
|
||||
private readonly driverRepository: IDriverRepository,
|
||||
public readonly presenter: IRaceProtestsPresenter,
|
||||
) {}
|
||||
|
||||
async execute(raceId: string): Promise<RaceProtestDTO[]> {
|
||||
async execute(raceId: string): Promise<void> {
|
||||
const protests = await this.protestRepository.findByRaceId(raceId);
|
||||
|
||||
// Load all driver details in parallel
|
||||
@@ -56,22 +40,6 @@ export class GetRaceProtestsQuery {
|
||||
}
|
||||
});
|
||||
|
||||
return protests.map(protest => ({
|
||||
id: protest.id,
|
||||
raceId: protest.raceId,
|
||||
protestingDriverId: protest.protestingDriverId,
|
||||
protestingDriverName: driverMap.get(protest.protestingDriverId) || 'Unknown',
|
||||
accusedDriverId: protest.accusedDriverId,
|
||||
accusedDriverName: driverMap.get(protest.accusedDriverId) || 'Unknown',
|
||||
incident: protest.incident,
|
||||
comment: protest.comment,
|
||||
proofVideoUrl: protest.proofVideoUrl,
|
||||
status: protest.status,
|
||||
reviewedBy: protest.reviewedBy,
|
||||
reviewedByName: protest.reviewedBy ? driverMap.get(protest.reviewedBy) : undefined,
|
||||
decisionNotes: protest.decisionNotes,
|
||||
filedAt: protest.filedAt.toISOString(),
|
||||
reviewedAt: protest.reviewedAt?.toISOString(),
|
||||
}));
|
||||
this.presenter.present(protests, driverMap);
|
||||
}
|
||||
}
|
||||
@@ -1,17 +1,22 @@
|
||||
import type { IRaceRegistrationRepository } from '@gridpilot/racing/domain/repositories/IRaceRegistrationRepository';
|
||||
import type { GetRaceRegistrationsQueryParamsDTO } from '../dto/RaceRegistrationQueryDTO';
|
||||
import type { IRaceRegistrationsPresenter } from '../presenters/IRaceRegistrationsPresenter';
|
||||
|
||||
/**
|
||||
* Query object returning registered driver IDs for a race.
|
||||
* Mirrors legacy getRegisteredDrivers behavior.
|
||||
* Use Case: GetRaceRegistrationsUseCase
|
||||
*
|
||||
* Returns registered driver IDs for a race.
|
||||
* Orchestrates domain logic and delegates presentation to the presenter.
|
||||
*/
|
||||
export class GetRaceRegistrationsQuery {
|
||||
export class GetRaceRegistrationsUseCase {
|
||||
constructor(
|
||||
private readonly registrationRepository: IRaceRegistrationRepository,
|
||||
public readonly presenter: IRaceRegistrationsPresenter,
|
||||
) {}
|
||||
|
||||
async execute(params: GetRaceRegistrationsQueryParamsDTO): Promise<string[]> {
|
||||
async execute(params: GetRaceRegistrationsQueryParamsDTO): Promise<void> {
|
||||
const { raceId } = params;
|
||||
return this.registrationRepository.getRegisteredDrivers(raceId);
|
||||
const registeredDriverIds = await this.registrationRepository.getRegisteredDrivers(raceId);
|
||||
this.presenter.present(registeredDriverIds);
|
||||
}
|
||||
}
|
||||
@@ -1,8 +1,9 @@
|
||||
/**
|
||||
* Application Query: GetRaceWithSOFQuery
|
||||
*
|
||||
* Use Case: GetRaceWithSOFUseCase
|
||||
*
|
||||
* Returns race details enriched with calculated Strength of Field (SOF).
|
||||
* SOF is calculated from participant ratings if not already stored on the race.
|
||||
* Orchestrates domain logic and delegates presentation to the presenter.
|
||||
*/
|
||||
|
||||
import type { IRaceRepository } from '../../domain/repositories/IRaceRepository';
|
||||
@@ -13,18 +14,13 @@ import {
|
||||
AverageStrengthOfFieldCalculator,
|
||||
type StrengthOfFieldCalculator,
|
||||
} from '../../domain/services/StrengthOfFieldCalculator';
|
||||
import type { RaceDTO } from '../dto/RaceDTO';
|
||||
import type { IRaceWithSOFPresenter } from '../presenters/IRaceWithSOFPresenter';
|
||||
|
||||
export interface GetRaceWithSOFQueryParams {
|
||||
raceId: string;
|
||||
}
|
||||
|
||||
export interface RaceWithSOFDTO extends Omit<RaceDTO, 'strengthOfField'> {
|
||||
strengthOfField: number | null;
|
||||
participantCount: number;
|
||||
}
|
||||
|
||||
export class GetRaceWithSOFQuery {
|
||||
export class GetRaceWithSOFUseCase {
|
||||
private readonly sofCalculator: StrengthOfFieldCalculator;
|
||||
|
||||
constructor(
|
||||
@@ -32,12 +28,13 @@ export class GetRaceWithSOFQuery {
|
||||
private readonly registrationRepository: IRaceRegistrationRepository,
|
||||
private readonly resultRepository: IResultRepository,
|
||||
private readonly driverRatingProvider: DriverRatingProvider,
|
||||
public readonly presenter: IRaceWithSOFPresenter,
|
||||
sofCalculator?: StrengthOfFieldCalculator,
|
||||
) {
|
||||
this.sofCalculator = sofCalculator ?? new AverageStrengthOfFieldCalculator();
|
||||
}
|
||||
|
||||
async execute(params: GetRaceWithSOFQueryParams): Promise<RaceWithSOFDTO | null> {
|
||||
async execute(params: GetRaceWithSOFQueryParams): Promise<void> {
|
||||
const { raceId } = params;
|
||||
|
||||
const race = await this.raceRepository.findById(raceId);
|
||||
@@ -69,20 +66,20 @@ export class GetRaceWithSOFQuery {
|
||||
strengthOfField = this.sofCalculator.calculate(driverRatings);
|
||||
}
|
||||
|
||||
return {
|
||||
id: race.id,
|
||||
leagueId: race.leagueId,
|
||||
scheduledAt: race.scheduledAt.toISOString(),
|
||||
track: race.track,
|
||||
trackId: race.trackId,
|
||||
car: race.car,
|
||||
carId: race.carId,
|
||||
sessionType: race.sessionType,
|
||||
status: race.status,
|
||||
this.presenter.present(
|
||||
race.id,
|
||||
race.leagueId,
|
||||
race.scheduledAt,
|
||||
race.track,
|
||||
race.trackId,
|
||||
race.car,
|
||||
race.carId,
|
||||
race.sessionType,
|
||||
race.status,
|
||||
strengthOfField,
|
||||
registeredCount: race.registeredCount ?? participantIds.length,
|
||||
maxParticipants: race.maxParticipants,
|
||||
participantCount: participantIds.length,
|
||||
};
|
||||
race.registeredCount ?? participantIds.length,
|
||||
race.maxParticipants,
|
||||
participantIds.length
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -1,6 +1,6 @@
|
||||
/**
|
||||
* Application Query: GetSponsorDashboardQuery
|
||||
*
|
||||
* Application Use Case: GetSponsorDashboardUseCase
|
||||
*
|
||||
* Returns sponsor dashboard metrics including sponsorships, impressions, and investment data.
|
||||
*/
|
||||
|
||||
@@ -10,6 +10,7 @@ import type { ISeasonRepository } from '../../domain/repositories/ISeasonReposit
|
||||
import type { ILeagueRepository } from '../../domain/repositories/ILeagueRepository';
|
||||
import type { ILeagueMembershipRepository } from '../../domain/repositories/ILeagueMembershipRepository';
|
||||
import type { IRaceRepository } from '../../domain/repositories/IRaceRepository';
|
||||
import type { ISponsorDashboardPresenter } from '../presenters/ISponsorDashboardPresenter';
|
||||
|
||||
export interface GetSponsorDashboardQueryParams {
|
||||
sponsorId: string;
|
||||
@@ -46,7 +47,7 @@ export interface SponsorDashboardDTO {
|
||||
};
|
||||
}
|
||||
|
||||
export class GetSponsorDashboardQuery {
|
||||
export class GetSponsorDashboardUseCase {
|
||||
constructor(
|
||||
private readonly sponsorRepository: ISponsorRepository,
|
||||
private readonly seasonSponsorshipRepository: ISeasonSponsorshipRepository,
|
||||
@@ -54,14 +55,16 @@ export class GetSponsorDashboardQuery {
|
||||
private readonly leagueRepository: ILeagueRepository,
|
||||
private readonly leagueMembershipRepository: ILeagueMembershipRepository,
|
||||
private readonly raceRepository: IRaceRepository,
|
||||
private readonly presenter: ISponsorDashboardPresenter,
|
||||
) {}
|
||||
|
||||
async execute(params: GetSponsorDashboardQueryParams): Promise<SponsorDashboardDTO | null> {
|
||||
async execute(params: GetSponsorDashboardQueryParams): Promise<void> {
|
||||
const { sponsorId } = params;
|
||||
|
||||
const sponsor = await this.sponsorRepository.findById(sponsorId);
|
||||
if (!sponsor) {
|
||||
return null;
|
||||
this.presenter.present(null);
|
||||
return;
|
||||
}
|
||||
|
||||
// Get all sponsorships for this sponsor
|
||||
@@ -140,7 +143,7 @@ export class GetSponsorDashboardQuery {
|
||||
? Math.min(100, (mainSponsorships * 30) + (sponsorships.length * 10))
|
||||
: 0;
|
||||
|
||||
return {
|
||||
this.presenter.present({
|
||||
sponsorId,
|
||||
sponsorName: sponsor.name,
|
||||
metrics: {
|
||||
@@ -159,6 +162,6 @@ export class GetSponsorDashboardQuery {
|
||||
totalInvestment,
|
||||
costPerThousandViews: Math.round(costPerThousandViews * 100) / 100,
|
||||
},
|
||||
};
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -1,6 +1,6 @@
|
||||
/**
|
||||
* Application Query: GetSponsorSponsorshipsQuery
|
||||
*
|
||||
* Application Use Case: GetSponsorSponsorshipsUseCase
|
||||
*
|
||||
* Returns detailed sponsorship information for a sponsor's campaigns/sponsorships page.
|
||||
*/
|
||||
|
||||
@@ -11,6 +11,7 @@ import type { ILeagueRepository } from '../../domain/repositories/ILeagueReposit
|
||||
import type { ILeagueMembershipRepository } from '../../domain/repositories/ILeagueMembershipRepository';
|
||||
import type { IRaceRepository } from '../../domain/repositories/IRaceRepository';
|
||||
import type { SponsorshipTier, SponsorshipStatus } from '../../domain/entities/SeasonSponsorship';
|
||||
import type { ISponsorSponsorshipsPresenter } from '../presenters/ISponsorSponsorshipsPresenter';
|
||||
|
||||
export interface GetSponsorSponsorshipsQueryParams {
|
||||
sponsorId: string;
|
||||
@@ -22,6 +23,8 @@ export interface SponsorshipDetailDTO {
|
||||
leagueName: string;
|
||||
seasonId: string;
|
||||
seasonName: string;
|
||||
seasonStartDate?: Date;
|
||||
seasonEndDate?: Date;
|
||||
tier: SponsorshipTier;
|
||||
status: SponsorshipStatus;
|
||||
pricing: {
|
||||
@@ -59,7 +62,7 @@ export interface SponsorSponsorshipsDTO {
|
||||
};
|
||||
}
|
||||
|
||||
export class GetSponsorSponsorshipsQuery {
|
||||
export class GetSponsorSponsorshipsUseCase {
|
||||
constructor(
|
||||
private readonly sponsorRepository: ISponsorRepository,
|
||||
private readonly seasonSponsorshipRepository: ISeasonSponsorshipRepository,
|
||||
@@ -67,14 +70,16 @@ export class GetSponsorSponsorshipsQuery {
|
||||
private readonly leagueRepository: ILeagueRepository,
|
||||
private readonly leagueMembershipRepository: ILeagueMembershipRepository,
|
||||
private readonly raceRepository: IRaceRepository,
|
||||
private readonly presenter: ISponsorSponsorshipsPresenter,
|
||||
) {}
|
||||
|
||||
async execute(params: GetSponsorSponsorshipsQueryParams): Promise<SponsorSponsorshipsDTO | null> {
|
||||
async execute(params: GetSponsorSponsorshipsQueryParams): Promise<void> {
|
||||
const { sponsorId } = params;
|
||||
|
||||
const sponsor = await this.sponsorRepository.findById(sponsorId);
|
||||
if (!sponsor) {
|
||||
return null;
|
||||
this.presenter.present(null);
|
||||
return;
|
||||
}
|
||||
|
||||
// Get all sponsorships for this sponsor
|
||||
@@ -116,6 +121,8 @@ export class GetSponsorSponsorshipsQuery {
|
||||
leagueName: league.name,
|
||||
seasonId: season.id,
|
||||
seasonName: season.name,
|
||||
seasonStartDate: season.startDate,
|
||||
seasonEndDate: season.endDate,
|
||||
tier: sponsorship.tier,
|
||||
status: sponsorship.status,
|
||||
pricing: {
|
||||
@@ -143,7 +150,7 @@ export class GetSponsorSponsorshipsQuery {
|
||||
|
||||
const activeSponsorships = sponsorships.filter(s => s.status === 'active').length;
|
||||
|
||||
return {
|
||||
this.presenter.present({
|
||||
sponsorId,
|
||||
sponsorName: sponsor.name,
|
||||
sponsorships: sponsorshipDetails,
|
||||
@@ -154,6 +161,6 @@ export class GetSponsorSponsorshipsQuery {
|
||||
totalPlatformFees,
|
||||
currency: 'USD',
|
||||
},
|
||||
};
|
||||
});
|
||||
}
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user