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