remove core from pages
This commit is contained in:
@@ -25,7 +25,7 @@ export default function LeagueDetailPage() {
|
||||
const params = useParams();
|
||||
const leagueId = params.id as string;
|
||||
const isSponsor = useSponsorMode();
|
||||
const { leagueService, leagueMembershipService } = useServices();
|
||||
const { leagueService, leagueMembershipService, raceService } = useServices();
|
||||
|
||||
const [viewModel, setViewModel] = useState<LeagueDetailPageViewModel | null>(null);
|
||||
const [loading, setLoading] = useState(true);
|
||||
@@ -168,8 +168,7 @@ export default function LeagueDetailPage() {
|
||||
<Calendar className="w-4 h-4" />
|
||||
<span>Started {new Date(race.date).toLocaleDateString()}</span>
|
||||
</div>
|
||||
{/* TODO: Add registeredCount and strengthOfField to RaceDTO */}
|
||||
{/* {race.registeredCount && (
|
||||
{race.registeredCount && (
|
||||
<div className="flex items-center gap-2">
|
||||
<Users className="w-4 h-4" />
|
||||
<span>{race.registeredCount} drivers registered</span>
|
||||
@@ -180,7 +179,7 @@ export default function LeagueDetailPage() {
|
||||
<Trophy className="w-4 h-4" />
|
||||
<span>SOF: {race.strengthOfField}</span>
|
||||
</div>
|
||||
)} */}
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
@@ -482,10 +481,7 @@ export default function LeagueDetailPage() {
|
||||
raceName={race.name}
|
||||
onConfirm={async () => {
|
||||
try {
|
||||
// TODO: Use service to complete race
|
||||
// const serviceFactory = new ServiceFactory(process.env.NEXT_PUBLIC_API_URL || '');
|
||||
// const raceService = serviceFactory.createRaceService();
|
||||
// await raceService.completeRace(race.id);
|
||||
await raceService.completeRace(race.id);
|
||||
await loadLeagueData();
|
||||
setEndRaceModalRaceId(null);
|
||||
} catch (err) {
|
||||
|
||||
@@ -3,9 +3,8 @@
|
||||
import { useState, useEffect } from 'react';
|
||||
import { useParams } from 'next/navigation';
|
||||
import Card from '@/components/ui/Card';
|
||||
import type { LeagueScoringConfigDTO } from '@core/racing/application/dto/LeagueScoringConfigDTO';
|
||||
import { useServices } from '@/lib/services/ServiceProvider';
|
||||
import type { LeagueWithCapacityDTO } from '@/lib/types/generated/LeagueWithCapacityDTO';
|
||||
import { LeagueDetailPageViewModel } from '@/lib/view-models/LeagueDetailPageViewModel';
|
||||
|
||||
type RulebookSection = 'scoring' | 'conduct' | 'protests' | 'penalties';
|
||||
|
||||
@@ -13,8 +12,7 @@ export default function LeagueRulebookPage() {
|
||||
const params = useParams();
|
||||
const leagueId = params.id as string;
|
||||
|
||||
const [league, setLeague] = useState<LeagueWithCapacityDTO | null>(null);
|
||||
const [scoringConfig, setScoringConfig] = useState<LeagueScoringConfigDTO | null>(null);
|
||||
const [viewModel, setViewModel] = useState<LeagueDetailPageViewModel | null>(null);
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [activeSection, setActiveSection] = useState<RulebookSection>('scoring');
|
||||
|
||||
@@ -22,14 +20,13 @@ export default function LeagueRulebookPage() {
|
||||
async function loadData() {
|
||||
try {
|
||||
const { leagueService } = useServices();
|
||||
const viewModel = await leagueService.getLeagueDetailPageData(leagueId);
|
||||
if (!viewModel) {
|
||||
const data = await leagueService.getLeagueDetailPageData(leagueId);
|
||||
if (!data) {
|
||||
setLoading(false);
|
||||
return;
|
||||
}
|
||||
|
||||
setLeague(viewModel.league);
|
||||
setScoringConfig(viewModel.scoringConfig);
|
||||
setViewModel(data);
|
||||
} catch (err) {
|
||||
console.error('Failed to load scoring config:', err);
|
||||
} finally {
|
||||
@@ -48,7 +45,7 @@ export default function LeagueRulebookPage() {
|
||||
);
|
||||
}
|
||||
|
||||
if (!league || !scoringConfig) {
|
||||
if (!viewModel || !viewModel.scoringConfig) {
|
||||
return (
|
||||
<Card>
|
||||
<div className="text-center py-12 text-gray-400">Unable to load rulebook</div>
|
||||
@@ -56,7 +53,7 @@ export default function LeagueRulebookPage() {
|
||||
);
|
||||
}
|
||||
|
||||
const primaryChampionship = scoringConfig.championships.find(c => c.type === 'driver') ?? scoringConfig.championships[0];
|
||||
const primaryChampionship = viewModel.scoringConfig.championships.find(c => c.type === 'driver') ?? viewModel.scoringConfig.championships[0];
|
||||
const positionPoints = primaryChampionship?.pointsPreview
|
||||
.filter(p => p.sessionType === primaryChampionship.sessionTypes[0])
|
||||
.map(p => ({ position: p.position, points: p.points }))
|
||||
@@ -78,7 +75,7 @@ export default function LeagueRulebookPage() {
|
||||
<p className="text-sm text-gray-400 mt-1">Official rules and regulations</p>
|
||||
</div>
|
||||
<div className="flex items-center gap-2 px-3 py-1.5 rounded-full bg-primary-blue/10 border border-primary-blue/20">
|
||||
<span className="text-sm font-medium text-primary-blue">{scoringConfig.scoringPresetName || 'Custom Rules'}</span>
|
||||
<span className="text-sm font-medium text-primary-blue">{viewModel.scoringConfig.scoringPresetName || 'Custom Rules'}</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -106,11 +103,11 @@ export default function LeagueRulebookPage() {
|
||||
<div className="grid grid-cols-2 md:grid-cols-4 gap-4">
|
||||
<div className="bg-iron-gray rounded-lg p-4 border border-charcoal-outline">
|
||||
<p className="text-xs text-gray-500 uppercase tracking-wider mb-1">Platform</p>
|
||||
<p className="text-lg font-semibold text-white">{scoringConfig.gameName}</p>
|
||||
<p className="text-lg font-semibold text-white">{viewModel.scoringConfig.gameName}</p>
|
||||
</div>
|
||||
<div className="bg-iron-gray rounded-lg p-4 border border-charcoal-outline">
|
||||
<p className="text-xs text-gray-500 uppercase tracking-wider mb-1">Championships</p>
|
||||
<p className="text-lg font-semibold text-white">{scoringConfig.championships.length}</p>
|
||||
<p className="text-lg font-semibold text-white">{viewModel.scoringConfig.championships.length}</p>
|
||||
</div>
|
||||
<div className="bg-iron-gray rounded-lg p-4 border border-charcoal-outline">
|
||||
<p className="text-xs text-gray-500 uppercase tracking-wider mb-1">Sessions Scored</p>
|
||||
@@ -120,8 +117,8 @@ export default function LeagueRulebookPage() {
|
||||
</div>
|
||||
<div className="bg-iron-gray rounded-lg p-4 border border-charcoal-outline">
|
||||
<p className="text-xs text-gray-500 uppercase tracking-wider mb-1">Drop Policy</p>
|
||||
<p className="text-lg font-semibold text-white truncate" title={scoringConfig.dropPolicySummary}>
|
||||
{scoringConfig.dropPolicySummary.includes('All') ? 'None' : 'Active'}
|
||||
<p className="text-lg font-semibold text-white truncate" title={viewModel.scoringConfig.dropPolicySummary}>
|
||||
{viewModel.scoringConfig.dropPolicySummary.includes('All') ? 'None' : 'Active'}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
@@ -192,10 +189,10 @@ export default function LeagueRulebookPage() {
|
||||
)}
|
||||
|
||||
{/* Drop Policy */}
|
||||
{!scoringConfig.dropPolicySummary.includes('All results count') && (
|
||||
{!viewModel.scoringConfig.dropPolicySummary.includes('All results count') && (
|
||||
<Card>
|
||||
<h2 className="text-lg font-semibold text-white mb-4">Drop Policy</h2>
|
||||
<p className="text-sm text-gray-300">{scoringConfig.dropPolicySummary}</p>
|
||||
<p className="text-sm text-gray-300">{viewModel.scoringConfig.dropPolicySummary}</p>
|
||||
<p className="text-xs text-gray-500 mt-3">
|
||||
Drop rules are applied automatically when calculating championship standings.
|
||||
</p>
|
||||
|
||||
@@ -4,6 +4,7 @@ import LeagueSchedule from '@/components/leagues/LeagueSchedule';
|
||||
import Card from '@/components/ui/Card';
|
||||
import { useEffectiveDriverId } from '@/hooks/useEffectiveDriverId';
|
||||
import { isLeagueAdminOrHigherRole } from '@/lib/leagueRoles';
|
||||
import { useServices } from '@/lib/services/ServiceProvider';
|
||||
import { useParams, useRouter } from 'next/navigation';
|
||||
import { useEffect, useState } from 'react';
|
||||
|
||||
@@ -12,17 +13,18 @@ export default function LeagueSchedulePage() {
|
||||
const router = useRouter();
|
||||
const leagueId = params.id as string;
|
||||
const currentDriverId = useEffectiveDriverId();
|
||||
const { leagueMembershipService } = useServices();
|
||||
const [isAdmin, setIsAdmin] = useState(false);
|
||||
const [showCreateForm, setShowCreateForm] = useState(false);
|
||||
|
||||
useEffect(() => {
|
||||
async function checkAdmin() {
|
||||
const membershipRepo = getLeagueMembershipRepository();
|
||||
const membership = await membershipRepo.getMembership(leagueId, currentDriverId);
|
||||
await leagueMembershipService.fetchLeagueMemberships(leagueId);
|
||||
const membership = leagueMembershipService.getMembership(leagueId, currentDriverId);
|
||||
setIsAdmin(membership ? isLeagueAdminOrHigherRole(membership.role) : false);
|
||||
}
|
||||
checkAdmin();
|
||||
}, [leagueId, currentDriverId]);
|
||||
}, [leagueId, currentDriverId, leagueMembershipService]);
|
||||
|
||||
return (
|
||||
<div className="space-y-6">
|
||||
|
||||
@@ -1,6 +0,0 @@
|
||||
import { redirect } from 'next/navigation';
|
||||
|
||||
export default async function ScoringPage({ params }: { params: Promise<{ id: string }> }) {
|
||||
const { id } = await params;
|
||||
redirect(`/leagues/${id}/rulebook`);
|
||||
}
|
||||
@@ -8,7 +8,6 @@ import { useEffectiveDriverId } from '@/hooks/useEffectiveDriverId';
|
||||
import { isLeagueAdminOrHigherRole } from '@/lib/leagueRoles';
|
||||
import { useServices } from '@/lib/services/ServiceProvider';
|
||||
import type { LeagueConfigFormModel } from '@/lib/types/LeagueConfigFormModel';
|
||||
import type { DriverDTO } from '@/lib/types/DriverDTO';
|
||||
import { LeagueSettingsViewModel } from '@/lib/view-models/LeagueSettingsViewModel';
|
||||
import { AlertTriangle, Settings, UserCog } from 'lucide-react';
|
||||
import { useParams, useRouter } from 'next/navigation';
|
||||
@@ -170,8 +169,8 @@ export default function LeagueSettingsPage() {
|
||||
>
|
||||
<option value="">Select new owner...</option>
|
||||
{settings.members.map((member) => (
|
||||
<option key={member.id} value={member.id}>
|
||||
{member.name}
|
||||
<option key={member.driver.id} value={member.driver.id}>
|
||||
{member.driver.name}
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
|
||||
@@ -4,7 +4,8 @@ import { LeagueSponsorshipsSection } from '@/components/leagues/LeagueSponsorshi
|
||||
import Card from '@/components/ui/Card';
|
||||
import { useEffectiveDriverId } from '@/hooks/useEffectiveDriverId';
|
||||
import { isLeagueAdminOrHigherRole } from '@/lib/leagueRoles';
|
||||
import type { League } from '@core/racing/domain/entities/League';
|
||||
import { useServices } from '@/lib/services/ServiceProvider';
|
||||
import { LeagueDetailViewModel } from '@/lib/view-models/LeagueDetailViewModel';
|
||||
import { AlertTriangle, Building } from 'lucide-react';
|
||||
import { useParams } from 'next/navigation';
|
||||
import { useEffect, useState } from 'react';
|
||||
@@ -13,23 +14,23 @@ export default function LeagueSponsorshipsPage() {
|
||||
const params = useParams();
|
||||
const leagueId = params.id as string;
|
||||
const currentDriverId = useEffectiveDriverId();
|
||||
const { leagueService, leagueMembershipService } = useServices();
|
||||
|
||||
const [league, setLeague] = useState<League | null>(null);
|
||||
const [league, setLeague] = useState<LeagueDetailViewModel | null>(null);
|
||||
const [isAdmin, setIsAdmin] = useState(false);
|
||||
const [loading, setLoading] = useState(true);
|
||||
|
||||
useEffect(() => {
|
||||
async function loadData() {
|
||||
try {
|
||||
const leagueRepo = getLeagueRepository();
|
||||
const membershipRepo = getLeagueMembershipRepository();
|
||||
|
||||
const [leagueData, membership] = await Promise.all([
|
||||
leagueRepo.findById(leagueId),
|
||||
membershipRepo.getMembership(leagueId, currentDriverId),
|
||||
const [leagueDetail, memberships] = await Promise.all([
|
||||
leagueService.getLeagueDetail(leagueId, currentDriverId),
|
||||
leagueMembershipService.fetchLeagueMemberships(leagueId),
|
||||
]);
|
||||
|
||||
setLeague(leagueData);
|
||||
const membership = leagueMembershipService.getMembership(leagueId, currentDriverId);
|
||||
|
||||
setLeague(leagueDetail);
|
||||
setIsAdmin(membership ? isLeagueAdminOrHigherRole(membership.role) : false);
|
||||
} catch (err) {
|
||||
console.error('Failed to load league:', err);
|
||||
@@ -39,7 +40,7 @@ export default function LeagueSponsorshipsPage() {
|
||||
}
|
||||
|
||||
loadData();
|
||||
}, [leagueId, currentDriverId]);
|
||||
}, [leagueId, currentDriverId, leagueService, leagueMembershipService]);
|
||||
|
||||
if (loading) {
|
||||
return (
|
||||
|
||||
@@ -3,9 +3,10 @@
|
||||
import StandingsTable from '@/components/leagues/StandingsTable';
|
||||
import Card from '@/components/ui/Card';
|
||||
import { useEffectiveDriverId } from '@/hooks/useEffectiveDriverId';
|
||||
import type { DriverDto, LeagueMembership } from '@/lib/dtos';
|
||||
import type { LeagueMembership, MembershipRole } from '@/lib/types';
|
||||
import { isLeagueAdminOrHigherRole } from '@/lib/leagueRoles';
|
||||
import { useServices } from '@/lib/services/ServiceProvider';
|
||||
import { DriverViewModel } from '@/lib/view-models';
|
||||
import type { LeagueStandingsViewModel } from '@/lib/view-models';
|
||||
import type { StandingEntryViewModel } from '@/lib/view-models/StandingEntryViewModel';
|
||||
import { useParams } from 'next/navigation';
|
||||
@@ -18,7 +19,7 @@ export default function LeagueStandingsPage() {
|
||||
const { leagueService } = useServices();
|
||||
|
||||
const [standings, setStandings] = useState<StandingEntryViewModel[]>([]);
|
||||
const [drivers, setDrivers] = useState<DriverDto[]>([]);
|
||||
const [drivers, setDrivers] = useState<DriverViewModel[]>([]);
|
||||
const [memberships, setMemberships] = useState<LeagueMembership[]>([]);
|
||||
const [viewModel, setViewModel] = useState<LeagueStandingsViewModel | null>(null);
|
||||
const [loading, setLoading] = useState(true);
|
||||
@@ -30,7 +31,7 @@ export default function LeagueStandingsPage() {
|
||||
const vm = await leagueService.getLeagueStandings(leagueId, currentDriverId);
|
||||
setViewModel(vm);
|
||||
setStandings(vm.standings);
|
||||
setDrivers(vm.drivers);
|
||||
setDrivers(vm.drivers.map(d => new DriverViewModel(d)));
|
||||
setMemberships(vm.memberships);
|
||||
|
||||
// Check if current user is admin
|
||||
@@ -53,21 +54,7 @@ export default function LeagueStandingsPage() {
|
||||
}
|
||||
|
||||
try {
|
||||
const membershipRepo = getLeagueMembershipRepository();
|
||||
const performer = await membershipRepo.getMembership(leagueId, currentDriverId);
|
||||
if (!performer || (performer.role !== 'owner' && performer.role !== 'admin')) {
|
||||
throw new Error('Only owners or admins can remove members');
|
||||
}
|
||||
|
||||
const membership = await membershipRepo.getMembership(leagueId, driverId);
|
||||
if (!membership) {
|
||||
throw new Error('Member not found');
|
||||
}
|
||||
if (membership.role === 'owner') {
|
||||
throw new Error('Cannot remove the league owner');
|
||||
}
|
||||
|
||||
await membershipRepo.removeMembership(leagueId, driverId);
|
||||
await leagueService.removeMember(leagueId, currentDriverId, driverId);
|
||||
await loadData();
|
||||
} catch (err) {
|
||||
setError(err instanceof Error ? err.message : 'Failed to remove member');
|
||||
@@ -76,25 +63,7 @@ export default function LeagueStandingsPage() {
|
||||
|
||||
const handleUpdateRole = async (driverId: string, newRole: MembershipRole) => {
|
||||
try {
|
||||
const membershipRepo = getLeagueMembershipRepository();
|
||||
const performer = await membershipRepo.getMembership(leagueId, currentDriverId);
|
||||
if (!performer || performer.role !== 'owner') {
|
||||
throw new Error('Only the league owner can update roles');
|
||||
}
|
||||
|
||||
const membership = await membershipRepo.getMembership(leagueId, driverId);
|
||||
if (!membership) {
|
||||
throw new Error('Member not found');
|
||||
}
|
||||
if (membership.role === 'owner') {
|
||||
throw new Error('Cannot change the owner role');
|
||||
}
|
||||
|
||||
await membershipRepo.saveMembership({
|
||||
...membership,
|
||||
role: newRole,
|
||||
});
|
||||
|
||||
await leagueService.updateMemberRole(leagueId, currentDriverId, driverId, newRole);
|
||||
await loadData();
|
||||
} catch (err) {
|
||||
setError(err instanceof Error ? err.message : 'Failed to update role');
|
||||
|
||||
@@ -6,12 +6,8 @@ import { ReviewProtestModal } from '@/components/leagues/ReviewProtestModal';
|
||||
import Button from '@/components/ui/Button';
|
||||
import Card from '@/components/ui/Card';
|
||||
import { useEffectiveDriverId } from '@/hooks/useEffectiveDriverId';
|
||||
import { isLeagueAdminOrHigherRole } from '@/lib/leagueRoles';
|
||||
import type { DriverDTO } from '@core/racing/application/dto/DriverDTO';
|
||||
import { EntityMappers } from '@core/racing/application/mappers/EntityMappers';
|
||||
import type { Penalty, PenaltyType } from '@core/racing/domain/entities/Penalty';
|
||||
import type { Protest } from '@core/racing/domain/entities/Protest';
|
||||
import type { Race } from '@core/racing/domain/entities/Race';
|
||||
import { useServices } from '@/lib/services/ServiceProvider';
|
||||
import { LeagueRoleUtility } from '@/lib/utilities/LeagueRoleUtility';
|
||||
import {
|
||||
AlertCircle,
|
||||
AlertTriangle,
|
||||
@@ -28,97 +24,98 @@ import Link from 'next/link';
|
||||
import { useParams } from 'next/navigation';
|
||||
import { useEffect, useMemo, useState } from 'react';
|
||||
|
||||
// Local type definitions to replace core imports
|
||||
type PenaltyType = 'time_penalty' | 'grid_penalty' | 'points_deduction' | 'disqualification' | 'warning' | 'license_points';
|
||||
|
||||
type DriverDTO = {
|
||||
id: string;
|
||||
name: string;
|
||||
avatarUrl?: string;
|
||||
iracingId?: string;
|
||||
rating?: number;
|
||||
};
|
||||
|
||||
interface RaceWithProtests {
|
||||
race: Race;
|
||||
pendingProtests: Protest[];
|
||||
resolvedProtests: Protest[];
|
||||
penalties: Penalty[];
|
||||
race: any;
|
||||
pendingProtests: any[];
|
||||
resolvedProtests: any[];
|
||||
penalties: any[];
|
||||
}
|
||||
|
||||
export default function LeagueStewardingPage() {
|
||||
const params = useParams();
|
||||
const leagueId = params.id as string;
|
||||
const currentDriverId = useEffectiveDriverId();
|
||||
const { raceService, protestService, driverService, leagueMembershipService, penaltyService } = useServices();
|
||||
|
||||
const [races, setRaces] = useState<Race[]>([]);
|
||||
const [protestsByRace, setProtestsByRace] = useState<Record<string, Protest[]>>({});
|
||||
const [penaltiesByRace, setPenaltiesByRace] = useState<Record<string, Penalty[]>>({});
|
||||
const [races, setRaces] = useState<any[]>([]);
|
||||
const [protestsByRace, setProtestsByRace] = useState<Record<string, any[]>>({});
|
||||
const [penaltiesByRace, setPenaltiesByRace] = useState<Record<string, any[]>>({});
|
||||
const [driversById, setDriversById] = useState<Record<string, DriverDTO>>({});
|
||||
const [allDrivers, setAllDrivers] = useState<DriverDTO[]>([]);
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [isAdmin, setIsAdmin] = useState(false);
|
||||
const [activeTab, setActiveTab] = useState<'pending' | 'history'>('pending');
|
||||
const [selectedProtest, setSelectedProtest] = useState<Protest | null>(null);
|
||||
const [selectedProtest, setSelectedProtest] = useState<any | null>(null);
|
||||
const [expandedRaces, setExpandedRaces] = useState<Set<string>>(new Set());
|
||||
const [showQuickPenaltyModal, setShowQuickPenaltyModal] = useState(false);
|
||||
|
||||
useEffect(() => {
|
||||
async function checkAdmin() {
|
||||
const membershipRepo = getLeagueMembershipRepository();
|
||||
const membership = await membershipRepo.getMembership(leagueId, currentDriverId);
|
||||
setIsAdmin(membership ? isLeagueAdminOrHigherRole(membership.role) : false);
|
||||
const membership = await leagueMembershipService.getMembership(leagueId, currentDriverId);
|
||||
setIsAdmin(membership ? LeagueRoleUtility.isLeagueAdminOrHigherRole(membership.role) : false);
|
||||
}
|
||||
checkAdmin();
|
||||
}, [leagueId, currentDriverId]);
|
||||
}, [leagueId, currentDriverId, leagueMembershipService]);
|
||||
|
||||
useEffect(() => {
|
||||
async function loadData() {
|
||||
setLoading(true);
|
||||
try {
|
||||
const raceRepo = getRaceRepository();
|
||||
const protestRepo = getProtestRepository();
|
||||
const penaltyRepo = getPenaltyRepository();
|
||||
const driverRepo = getDriverRepository();
|
||||
|
||||
// Get all races for this league
|
||||
const leagueRaces = await raceRepo.findByLeagueId(leagueId);
|
||||
const leagueRaces = await raceService.findByLeagueId(leagueId);
|
||||
setRaces(leagueRaces);
|
||||
|
||||
|
||||
// Get protests and penalties for each race
|
||||
const protestsMap: Record<string, Protest[]> = {};
|
||||
const penaltiesMap: Record<string, Penalty[]> = {};
|
||||
const protestsMap: Record<string, any[]> = {};
|
||||
const penaltiesMap: Record<string, any[]> = {};
|
||||
const driverIds = new Set<string>();
|
||||
|
||||
|
||||
for (const race of leagueRaces) {
|
||||
const raceProtests = await protestRepo.findByRaceId(race.id);
|
||||
const racePenalties = await penaltyRepo.findByRaceId(race.id);
|
||||
|
||||
const raceProtests = await protestService.findByRaceId(race.id);
|
||||
const racePenalties = await penaltyService.findByRaceId(race.id);
|
||||
|
||||
protestsMap[race.id] = raceProtests;
|
||||
penaltiesMap[race.id] = racePenalties;
|
||||
|
||||
|
||||
// Collect driver IDs
|
||||
raceProtests.forEach((p) => {
|
||||
raceProtests.forEach((p: any) => {
|
||||
driverIds.add(p.protestingDriverId);
|
||||
driverIds.add(p.accusedDriverId);
|
||||
});
|
||||
racePenalties.forEach((p) => {
|
||||
racePenalties.forEach((p: any) => {
|
||||
driverIds.add(p.driverId);
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
setProtestsByRace(protestsMap);
|
||||
setPenaltiesByRace(penaltiesMap);
|
||||
|
||||
// Load driver info
|
||||
const driverEntities = await Promise.all(
|
||||
Array.from(driverIds).map((id) => driverRepo.findById(id)),
|
||||
);
|
||||
const byId: Record<string, DriverDTO> = {};
|
||||
const driverEntities = await driverService.findByIds(Array.from(driverIds));
|
||||
const byId: Record<string, any> = {};
|
||||
driverEntities.forEach((driver) => {
|
||||
if (driver) {
|
||||
const dto = EntityMappers.toDriverDTO(driver);
|
||||
if (dto) {
|
||||
byId[dto.id] = dto;
|
||||
}
|
||||
byId[driver.id] = driver;
|
||||
}
|
||||
});
|
||||
setDriversById(byId);
|
||||
setAllDrivers(Object.values(byId));
|
||||
|
||||
|
||||
// Auto-expand races with pending protests
|
||||
const racesWithPending = new Set<string>();
|
||||
Object.entries(protestsMap).forEach(([raceId, protests]) => {
|
||||
if (protests.some(p => p.status === 'pending' || p.status === 'under_review')) {
|
||||
if (protests.some((p: any) => p.status === 'pending' || p.status === 'under_review')) {
|
||||
racesWithPending.add(raceId);
|
||||
}
|
||||
});
|
||||
@@ -133,7 +130,7 @@ export default function LeagueStewardingPage() {
|
||||
if (isAdmin) {
|
||||
loadData();
|
||||
}
|
||||
}, [leagueId, isAdmin]);
|
||||
}, [leagueId, isAdmin, raceService, protestService, driverService, penaltyService]);
|
||||
|
||||
// Compute race data with protest/penalty info
|
||||
const racesWithData = useMemo((): RaceWithProtests[] => {
|
||||
@@ -168,10 +165,7 @@ export default function LeagueStewardingPage() {
|
||||
penaltyValue: number,
|
||||
stewardNotes: string
|
||||
) => {
|
||||
const reviewUseCase = getReviewProtestUseCase();
|
||||
const penaltyUseCase = getApplyPenaltyUseCase();
|
||||
|
||||
await reviewUseCase.execute({
|
||||
await protestService.reviewProtest({
|
||||
protestId,
|
||||
stewardId: currentDriverId,
|
||||
decision: 'uphold',
|
||||
@@ -179,14 +173,14 @@ export default function LeagueStewardingPage() {
|
||||
});
|
||||
|
||||
// Find the protest
|
||||
let foundProtest: Protest | undefined;
|
||||
let foundProtest: any | undefined;
|
||||
Object.values(protestsByRace).forEach(protests => {
|
||||
const p = protests.find(pr => pr.id === protestId);
|
||||
if (p) foundProtest = p;
|
||||
});
|
||||
|
||||
if (foundProtest) {
|
||||
await penaltyUseCase.execute({
|
||||
await penaltyService.applyPenalty({
|
||||
raceId: foundProtest.raceId,
|
||||
driverId: foundProtest.accusedDriverId,
|
||||
stewardId: currentDriverId,
|
||||
@@ -200,9 +194,7 @@ export default function LeagueStewardingPage() {
|
||||
};
|
||||
|
||||
const handleRejectProtest = async (protestId: string, stewardNotes: string) => {
|
||||
const reviewUseCase = getReviewProtestUseCase();
|
||||
|
||||
await reviewUseCase.execute({
|
||||
await protestService.reviewProtest({
|
||||
protestId,
|
||||
stewardId: currentDriverId,
|
||||
decision: 'dismiss',
|
||||
@@ -210,10 +202,6 @@ export default function LeagueStewardingPage() {
|
||||
});
|
||||
};
|
||||
|
||||
const handleProtestReviewed = () => {
|
||||
setSelectedProtest(null);
|
||||
window.location.reload();
|
||||
};
|
||||
|
||||
const toggleRaceExpanded = (raceId: string) => {
|
||||
setExpandedRaces(prev => {
|
||||
|
||||
@@ -3,12 +3,12 @@
|
||||
import Button from '@/components/ui/Button';
|
||||
import Card from '@/components/ui/Card';
|
||||
import { useEffectiveDriverId } from '@/hooks/useEffectiveDriverId';
|
||||
import { isLeagueAdminOrHigherRole } from '@/lib/leagueRoles';
|
||||
import { LeagueRoleUtility } from '@/lib/utilities/LeagueRoleUtility';
|
||||
import { useServices } from '@/lib/services/ServiceProvider';
|
||||
import { ProtestViewModel } from '@/lib/view-models/ProtestViewModel';
|
||||
import { RaceViewModel } from '@/lib/view-models/RaceViewModel';
|
||||
import { ProtestDriverViewModel } from '@/lib/view-models/ProtestDriverViewModel';
|
||||
import { ProtestDecisionCommandModel, type PenaltyType } from '@/lib/command-models/protests/ProtestDecisionCommandModel';
|
||||
import type { DriverSummaryDTO } from '@/lib/types/generated/LeagueAdminProtestsDTO';
|
||||
import type { RaceDTO } from '@/lib/types/generated/RaceDTO';
|
||||
import {
|
||||
AlertCircle,
|
||||
AlertTriangle,
|
||||
@@ -40,7 +40,7 @@ interface TimelineEvent {
|
||||
id: string;
|
||||
type: 'protest_filed' | 'defense_requested' | 'defense_submitted' | 'steward_comment' | 'decision' | 'penalty_applied';
|
||||
timestamp: Date;
|
||||
actor: DriverDTO | null;
|
||||
actor: ProtestDriverViewModel | null;
|
||||
content: string;
|
||||
metadata?: Record<string, unknown>;
|
||||
}
|
||||
@@ -114,12 +114,12 @@ export default function ProtestReviewPage() {
|
||||
const leagueId = params.id as string;
|
||||
const protestId = params.protestId as string;
|
||||
const currentDriverId = useEffectiveDriverId();
|
||||
const { protestService } = useServices();
|
||||
const { protestService, leagueMembershipService } = useServices();
|
||||
|
||||
const [protest, setProtest] = useState<ProtestViewModel | null>(null);
|
||||
const [race, setRace] = useState<RaceDTO | null>(null);
|
||||
const [protestingDriver, setProtestingDriver] = useState<DriverSummaryDTO | null>(null);
|
||||
const [accusedDriver, setAccusedDriver] = useState<DriverSummaryDTO | null>(null);
|
||||
const [race, setRace] = useState<RaceViewModel | null>(null);
|
||||
const [protestingDriver, setProtestingDriver] = useState<ProtestDriverViewModel | null>(null);
|
||||
const [accusedDriver, setAccusedDriver] = useState<ProtestDriverViewModel | null>(null);
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [isAdmin, setIsAdmin] = useState(false);
|
||||
|
||||
@@ -136,12 +136,12 @@ export default function ProtestReviewPage() {
|
||||
|
||||
useEffect(() => {
|
||||
async function checkAdmin() {
|
||||
const membershipRepo = getLeagueMembershipRepository();
|
||||
const membership = await membershipRepo.getMembership(leagueId, currentDriverId);
|
||||
setIsAdmin(membership ? isLeagueAdminOrHigherRole(membership.role) : false);
|
||||
await leagueMembershipService.fetchLeagueMemberships(leagueId);
|
||||
const membership = leagueMembershipService.getMembership(leagueId, currentDriverId);
|
||||
setIsAdmin(membership ? LeagueRoleUtility.isLeagueAdminOrHigherRole(membership.role) : false);
|
||||
}
|
||||
checkAdmin();
|
||||
}, [leagueId, currentDriverId]);
|
||||
}, [leagueId, currentDriverId, leagueMembershipService]);
|
||||
|
||||
useEffect(() => {
|
||||
async function loadProtest() {
|
||||
@@ -188,19 +188,19 @@ export default function ProtestReviewPage() {
|
||||
}
|
||||
];
|
||||
|
||||
// TODO: Add decision event when status/decisions are available in DTO
|
||||
// if (protest.status === 'upheld' || protest.status === 'dismissed') {
|
||||
// events.push({
|
||||
// id: 'decision',
|
||||
// type: 'decision',
|
||||
// timestamp: protest.reviewedAt ? new Date(protest.reviewedAt) : new Date(),
|
||||
// actor: null, // Would need to load steward driver
|
||||
// content: protest.decisionNotes || (protest.status === 'upheld' ? 'Protest upheld' : 'Protest dismissed'),
|
||||
// metadata: {
|
||||
// decision: protest.status
|
||||
// }
|
||||
// });
|
||||
// }
|
||||
// Add decision event when status/decisions are available in view model
|
||||
if (protest.status === 'upheld' || protest.status === 'dismissed') {
|
||||
events.push({
|
||||
id: 'decision',
|
||||
type: 'decision',
|
||||
timestamp: protest.reviewedAt ? new Date(protest.reviewedAt) : new Date(),
|
||||
actor: null, // Would need to load steward driver
|
||||
content: protest.decisionNotes || (protest.status === 'upheld' ? 'Protest upheld' : 'Protest dismissed'),
|
||||
metadata: {
|
||||
decision: protest.status
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
return events.sort((a, b) => a.timestamp.getTime() - b.timestamp.getTime());
|
||||
}, [protest, protestingDriver]);
|
||||
@@ -315,9 +315,9 @@ export default function ProtestReviewPage() {
|
||||
);
|
||||
}
|
||||
|
||||
const statusConfig = getStatusConfig('pending'); // TODO: Update when status is available
|
||||
const statusConfig = getStatusConfig(protest.status);
|
||||
const StatusIcon = statusConfig.icon;
|
||||
const isPending = true; // TODO: Update when status is available
|
||||
const isPending = protest.status === 'pending';
|
||||
const daysSinceFiled = Math.floor((Date.now() - new Date(protest.submittedAt).getTime()) / (1000 * 60 * 60 * 24));
|
||||
|
||||
return (
|
||||
@@ -404,7 +404,7 @@ export default function ProtestReviewPage() {
|
||||
</div>
|
||||
<div className="flex items-center gap-2 text-sm">
|
||||
<Calendar className="w-4 h-4 text-gray-500" />
|
||||
<span className="text-gray-300">{new Date(race.date).toLocaleDateString()}</span>
|
||||
<span className="text-gray-300">{race.formattedDate}</span>
|
||||
</div>
|
||||
{/* TODO: Add lap info when available */}
|
||||
{/* <div className="flex items-center gap-2 text-sm">
|
||||
|
||||
@@ -400,7 +400,6 @@ export default function LeaguesPage() {
|
||||
}
|
||||
};
|
||||
|
||||
// Use only real leagues from repository
|
||||
const leagues = realLeagues;
|
||||
|
||||
const handleLeagueClick = (leagueId: string) => {
|
||||
|
||||
@@ -3,13 +3,14 @@
|
||||
import Button from '@/components/ui/Button';
|
||||
import Card from '@/components/ui/Card';
|
||||
import { useEffectiveDriverId } from '@/hooks/useEffectiveDriverId';
|
||||
import type { League } from '@core/racing/domain/entities/League';
|
||||
import type { LeagueMembership } from '@core/racing/domain/entities/LeagueMembership';
|
||||
import { useServices } from '@/lib/services/ServiceProvider';
|
||||
import type { LeagueMembership } from '@/lib/types/LeagueMembership';
|
||||
import type { LeagueSummaryViewModel } from '@/lib/view-models/LeagueSummaryViewModel';
|
||||
import Link from 'next/link';
|
||||
import { useEffect, useState } from 'react';
|
||||
|
||||
interface LeagueWithRole {
|
||||
league: League;
|
||||
league: LeagueSummaryViewModel;
|
||||
membership: LeagueMembership;
|
||||
}
|
||||
|
||||
@@ -19,6 +20,7 @@ export default function ManageLeaguesPage() {
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
const effectiveDriverId = useEffectiveDriverId();
|
||||
const { leagueService, leagueMembershipService } = useServices();
|
||||
|
||||
useEffect(() => {
|
||||
let cancelled = false;
|
||||
@@ -26,14 +28,12 @@ export default function ManageLeaguesPage() {
|
||||
const load = async () => {
|
||||
setLoading(true);
|
||||
try {
|
||||
const leagueRepo = getLeagueRepository();
|
||||
const membershipRepo = getLeagueMembershipRepository();
|
||||
|
||||
const leagues = await leagueRepo.findAll();
|
||||
const leagues = await leagueService.getAllLeagues();
|
||||
|
||||
const memberships = await Promise.all(
|
||||
leagues.map(async (league) => {
|
||||
const membership = await membershipRepo.getMembership(league.id, effectiveDriverId);
|
||||
await leagueMembershipService.fetchLeagueMemberships(league.id);
|
||||
const membership = leagueMembershipService.getMembership(league.id, effectiveDriverId);
|
||||
return { league, membership };
|
||||
}),
|
||||
);
|
||||
@@ -76,7 +76,7 @@ export default function ManageLeaguesPage() {
|
||||
return () => {
|
||||
cancelled = true;
|
||||
};
|
||||
}, [effectiveDriverId]);
|
||||
}, [effectiveDriverId, leagueService, leagueMembershipService]);
|
||||
|
||||
if (loading) {
|
||||
return (
|
||||
|
||||
@@ -7,12 +7,12 @@ import Button from '@/components/ui/Button';
|
||||
import Card from '@/components/ui/Card';
|
||||
import Heading from '@/components/ui/Heading';
|
||||
import { useEffectiveDriverId } from '@/hooks/useEffectiveDriverId';
|
||||
import type { DriverDTO } from '@core/racing/application/dto/DriverDTO';
|
||||
import { useServices } from '@/lib/services/ServiceProvider';
|
||||
import type {
|
||||
ProfileOverviewAchievementViewModel,
|
||||
ProfileOverviewSocialHandleViewModel,
|
||||
ProfileOverviewViewModel
|
||||
} from '@/lib/view-models/ProfileOverviewViewModel';
|
||||
DriverProfileAchievementViewModel,
|
||||
DriverProfileSocialHandleViewModel,
|
||||
DriverProfileViewModel
|
||||
} from '@/lib/view-models/DriverProfileViewModel';
|
||||
import {
|
||||
Activity,
|
||||
Award,
|
||||
@@ -67,7 +67,7 @@ function getCountryFlag(countryCode: string): string {
|
||||
return '🏁';
|
||||
}
|
||||
|
||||
function getRarityColor(rarity: ProfileOverviewAchievementViewModel['rarity']) {
|
||||
function getRarityColor(rarity: DriverProfileAchievementViewModel['rarity']) {
|
||||
switch (rarity) {
|
||||
case 'common':
|
||||
return 'text-gray-400 bg-gray-400/10 border-gray-400/30';
|
||||
@@ -80,7 +80,7 @@ function getRarityColor(rarity: ProfileOverviewAchievementViewModel['rarity']) {
|
||||
}
|
||||
}
|
||||
|
||||
function getAchievementIcon(icon: ProfileOverviewAchievementViewModel['icon']) {
|
||||
function getAchievementIcon(icon: DriverProfileAchievementViewModel['icon']) {
|
||||
switch (icon) {
|
||||
case 'trophy':
|
||||
return Trophy;
|
||||
@@ -97,7 +97,7 @@ function getAchievementIcon(icon: ProfileOverviewAchievementViewModel['icon']) {
|
||||
}
|
||||
}
|
||||
|
||||
function getSocialIcon(platform: ProfileOverviewSocialHandleViewModel['platform']) {
|
||||
function getSocialIcon(platform: DriverProfileSocialHandleViewModel['platform']) {
|
||||
switch (platform) {
|
||||
case 'twitter':
|
||||
return Twitter;
|
||||
@@ -110,7 +110,7 @@ function getSocialIcon(platform: ProfileOverviewSocialHandleViewModel['platform'
|
||||
}
|
||||
}
|
||||
|
||||
function getSocialColor(platform: ProfileOverviewSocialHandleViewModel['platform']) {
|
||||
function getSocialColor(platform: DriverProfileSocialHandleViewModel['platform']) {
|
||||
switch (platform) {
|
||||
case 'twitter':
|
||||
return 'hover:text-sky-400 hover:bg-sky-400/10';
|
||||
@@ -256,12 +256,13 @@ export default function ProfilePage() {
|
||||
const router = useRouter();
|
||||
const searchParams = useSearchParams();
|
||||
const tabParam = searchParams.get('tab') as ProfileTab | null;
|
||||
|
||||
const [driver, setDriver] = useState<DriverDTO | null>(null);
|
||||
|
||||
const { driverService, mediaService } = useServices();
|
||||
|
||||
const [profileData, setProfileData] = useState<DriverProfileViewModel | null>(null);
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [editMode, setEditMode] = useState(false);
|
||||
const [activeTab, setActiveTab] = useState<ProfileTab>(tabParam || 'overview');
|
||||
const [profileData, setProfileData] = useState<ProfileOverviewViewModel | null>(null);
|
||||
const [friendRequestSent, setFriendRequestSent] = useState(false);
|
||||
|
||||
const effectiveDriverId = useEffectiveDriverId();
|
||||
@@ -271,24 +272,8 @@ export default function ProfilePage() {
|
||||
const loadData = async () => {
|
||||
try {
|
||||
const currentDriverId = effectiveDriverId;
|
||||
|
||||
// Use GetProfileOverviewUseCase to load all profile data
|
||||
const profileUseCase = getGetProfileOverviewUseCase();
|
||||
const profileViewModel = await profileUseCase.execute({ driverId: currentDriverId });
|
||||
|
||||
if (profileViewModel && profileViewModel.currentDriver) {
|
||||
// Set driver from ViewModel instead of direct repository access
|
||||
const driverData: DriverDTO = {
|
||||
id: profileViewModel.currentDriver.id,
|
||||
name: profileViewModel.currentDriver.name,
|
||||
iracingId: profileViewModel.currentDriver.iracingId ?? '',
|
||||
country: profileViewModel.currentDriver.country,
|
||||
bio: profileViewModel.currentDriver.bio || '',
|
||||
joinedAt: profileViewModel.currentDriver.joinedAt,
|
||||
};
|
||||
setDriver(driverData);
|
||||
setProfileData(profileViewModel);
|
||||
}
|
||||
const profileViewModel = await driverService.getDriverProfile(currentDriverId);
|
||||
setProfileData(profileViewModel);
|
||||
} catch (error) {
|
||||
console.error('Failed to load profile:', error);
|
||||
} finally {
|
||||
@@ -296,7 +281,7 @@ export default function ProfilePage() {
|
||||
}
|
||||
};
|
||||
void loadData();
|
||||
}, [effectiveDriverId]);
|
||||
}, [effectiveDriverId, driverService]);
|
||||
|
||||
// Update URL when tab changes
|
||||
useEffect(() => {
|
||||
@@ -319,24 +304,13 @@ export default function ProfilePage() {
|
||||
}
|
||||
}, [tabParam]);
|
||||
|
||||
const handleSaveSettings = async (updates: Partial<DriverDTO>) => {
|
||||
if (!driver) return;
|
||||
const handleSaveSettings = async (updates: { bio?: string; country?: string }) => {
|
||||
if (!profileData?.currentDriver) return;
|
||||
|
||||
try {
|
||||
const updateProfileUseCase = getUpdateDriverProfileUseCase();
|
||||
const input: { driverId: string; bio?: string; country?: string } = { driverId: driver.id };
|
||||
if (typeof updates.bio === 'string') {
|
||||
input.bio = updates.bio;
|
||||
}
|
||||
if (typeof updates.country === 'string') {
|
||||
input.country = updates.country;
|
||||
}
|
||||
const updatedDto = await updateProfileUseCase.execute(input);
|
||||
|
||||
if (updatedDto) {
|
||||
setDriver(updatedDto);
|
||||
setEditMode(false);
|
||||
}
|
||||
const updatedProfile = await driverService.updateProfile(updates);
|
||||
setProfileData(updatedProfile);
|
||||
setEditMode(false);
|
||||
} catch (error) {
|
||||
console.error('Failed to update profile:', error);
|
||||
}
|
||||
@@ -360,7 +334,7 @@ export default function ProfilePage() {
|
||||
);
|
||||
}
|
||||
|
||||
if (!driver) {
|
||||
if (!profileData?.currentDriver) {
|
||||
return (
|
||||
<div className="max-w-4xl mx-auto px-4">
|
||||
<div className="text-center mb-8">
|
||||
@@ -387,12 +361,12 @@ export default function ProfilePage() {
|
||||
}
|
||||
|
||||
// Extract data from profileData ViewModel
|
||||
const currentDriver = profileData?.currentDriver || null;
|
||||
const stats = profileData?.stats || null;
|
||||
const finishDistribution = profileData?.finishDistribution || null;
|
||||
const teamMemberships = profileData?.teamMemberships || [];
|
||||
const socialSummary = profileData?.socialSummary || { friendsCount: 0, friends: [] };
|
||||
const extendedProfile = profileData?.extendedProfile;
|
||||
const currentDriver = profileData.currentDriver;
|
||||
const stats = profileData.stats;
|
||||
const finishDistribution = profileData.finishDistribution;
|
||||
const teamMemberships = profileData.teamMemberships;
|
||||
const socialSummary = profileData.socialSummary;
|
||||
const extendedProfile = profileData.extendedProfile;
|
||||
const globalRank = currentDriver?.globalRank || null;
|
||||
|
||||
// Show edit mode
|
||||
@@ -405,7 +379,7 @@ export default function ProfilePage() {
|
||||
Cancel
|
||||
</Button>
|
||||
</div>
|
||||
<ProfileSettings driver={driver} onSave={handleSaveSettings} />
|
||||
<ProfileSettings driver={profileData.currentDriver} onSave={handleSaveSettings} />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -428,8 +402,8 @@ export default function ProfilePage() {
|
||||
<div className="w-28 h-28 md:w-36 md:h-36 rounded-2xl bg-gradient-to-br from-primary-blue to-purple-600 p-1 shadow-xl shadow-primary-blue/20">
|
||||
<div className="w-full h-full rounded-xl overflow-hidden bg-iron-gray">
|
||||
<Image
|
||||
src={getImageService().getDriverAvatar(driver.id)}
|
||||
alt={driver.name}
|
||||
src={mediaService.getDriverAvatar(currentDriver.id)}
|
||||
alt={currentDriver.name}
|
||||
width={144}
|
||||
height={144}
|
||||
className="w-full h-full object-cover"
|
||||
@@ -443,9 +417,9 @@ export default function ProfilePage() {
|
||||
{/* Driver Info */}
|
||||
<div className="flex-1 min-w-0">
|
||||
<div className="flex flex-wrap items-center gap-3 mb-2">
|
||||
<h1 className="text-3xl md:text-4xl font-bold text-white">{driver.name}</h1>
|
||||
<span className="text-4xl" aria-label={`Country: ${driver.country}`}>
|
||||
{getCountryFlag(driver.country)}
|
||||
<h1 className="text-3xl md:text-4xl font-bold text-white">{currentDriver.name}</h1>
|
||||
<span className="text-4xl" aria-label={`Country: ${currentDriver.country}`}>
|
||||
{getCountryFlag(currentDriver.country)}
|
||||
</span>
|
||||
{teamMemberships.length > 0 && teamMemberships[0] && (
|
||||
<span className="px-3 py-1 bg-purple-600/20 text-purple-400 rounded-full text-sm font-semibold border border-purple-600/30">
|
||||
@@ -488,11 +462,11 @@ export default function ProfilePage() {
|
||||
<div className="flex flex-wrap items-center gap-4 text-sm text-gray-400">
|
||||
<span className="flex items-center gap-1.5">
|
||||
<Globe className="w-4 h-4" />
|
||||
iRacing: {driver.iracingId}
|
||||
iRacing: {currentDriver.iracingId}
|
||||
</span>
|
||||
<span className="flex items-center gap-1.5">
|
||||
<Calendar className="w-4 h-4" />
|
||||
Joined {new Date(driver.joinedAt).toLocaleDateString('en-US', { month: 'short', year: 'numeric' })}
|
||||
Joined {new Date(currentDriver.joinedAt).toLocaleDateString('en-US', { month: 'short', year: 'numeric' })}
|
||||
</span>
|
||||
{extendedProfile && (
|
||||
<span className="flex items-center gap-1.5">
|
||||
@@ -564,13 +538,13 @@ export default function ProfilePage() {
|
||||
</div>
|
||||
|
||||
{/* Bio Section */}
|
||||
{driver.bio && (
|
||||
{currentDriver.bio && (
|
||||
<Card>
|
||||
<h2 className="text-lg font-semibold text-white mb-3 flex items-center gap-2">
|
||||
<User className="w-5 h-5 text-primary-blue" />
|
||||
About
|
||||
</h2>
|
||||
<p className="text-gray-300 leading-relaxed">{driver.bio}</p>
|
||||
<p className="text-gray-300 leading-relaxed">{currentDriver.bio}</p>
|
||||
</Card>
|
||||
)}
|
||||
|
||||
@@ -910,7 +884,7 @@ export default function ProfilePage() {
|
||||
>
|
||||
<div className="w-8 h-8 rounded-full overflow-hidden bg-gradient-to-br from-primary-blue to-purple-600">
|
||||
<Image
|
||||
src={getImageService().getDriverAvatar(friend.id)}
|
||||
src={mediaService.getDriverAvatar(friend.id)}
|
||||
alt={friend.name}
|
||||
width={32}
|
||||
height={32}
|
||||
@@ -932,13 +906,13 @@ export default function ProfilePage() {
|
||||
</>
|
||||
)}
|
||||
|
||||
{activeTab === 'history' && driver && (
|
||||
{activeTab === 'history' && currentDriver && (
|
||||
<Card>
|
||||
<h2 className="text-lg font-semibold text-white mb-4 flex items-center gap-2">
|
||||
<History className="w-5 h-5 text-red-400" />
|
||||
Race History
|
||||
</h2>
|
||||
<ProfileRaceHistory driverId={driver.id} />
|
||||
<ProfileRaceHistory driverId={currentDriver.id} />
|
||||
</Card>
|
||||
)}
|
||||
|
||||
|
||||
@@ -1,15 +1,16 @@
|
||||
'use client';
|
||||
|
||||
import Breadcrumbs from '@/components/layout/Breadcrumbs';
|
||||
import PendingSponsorshipRequests, { type PendingRequestDTO } from '@/components/sponsors/PendingSponsorshipRequests';
|
||||
import PendingSponsorshipRequests from '@/components/sponsors/PendingSponsorshipRequests';
|
||||
import Button from '@/components/ui/Button';
|
||||
import Card from '@/components/ui/Card';
|
||||
import { useRouter } from 'next/navigation';
|
||||
import { useCallback, useEffect, useState } from 'react';
|
||||
|
||||
import { useEffectiveDriverId } from '@/hooks/useEffectiveDriverId';
|
||||
import { isLeagueAdminOrHigherRole } from '@/lib/leagueRoles';
|
||||
import { LeagueRoleUtility } from '@/lib/utilities/LeagueRoleUtility';
|
||||
import { useServices } from '@/lib/services/ServiceProvider';
|
||||
import { SponsorshipRequestViewModel } from '@/lib/view-models/SponsorshipRequestViewModel';
|
||||
import { AlertTriangle, Building, ChevronRight, Handshake, Trophy, User, Users } from 'lucide-react';
|
||||
import Link from 'next/link';
|
||||
|
||||
@@ -17,7 +18,7 @@ interface EntitySection {
|
||||
entityType: 'driver' | 'team' | 'race' | 'season';
|
||||
entityId: string;
|
||||
entityName: string;
|
||||
requests: PendingRequestDTO[];
|
||||
requests: SponsorshipRequestViewModel[];
|
||||
}
|
||||
|
||||
export default function SponsorshipRequestsPage() {
|
||||
@@ -33,50 +34,45 @@ export default function SponsorshipRequestsPage() {
|
||||
setError(null);
|
||||
|
||||
try {
|
||||
const { sponsorshipService } = useServices();
|
||||
const driverRepo = getDriverRepository();
|
||||
const leagueRepo = getLeagueRepository();
|
||||
const teamRepo = getTeamRepository();
|
||||
const leagueMembershipRepo = getLeagueMembershipRepository();
|
||||
const teamMembershipRepo = getTeamMembershipRepository();
|
||||
|
||||
const { sponsorshipService, driverService, leagueService, teamService, leagueMembershipService } = useServices();
|
||||
|
||||
const allSections: EntitySection[] = [];
|
||||
|
||||
|
||||
// 1. Driver's own sponsorship requests
|
||||
const driverResult = await sponsorshipService.getPendingSponsorshipRequests({
|
||||
const driverRequests = await sponsorshipService.getPendingSponsorshipRequests({
|
||||
entityType: 'driver',
|
||||
entityId: currentDriverId,
|
||||
});
|
||||
|
||||
if (driverResult && driverResult.requests.length > 0) {
|
||||
const driver = await driverRepo.findById(currentDriverId);
|
||||
|
||||
if (driverRequests.length > 0) {
|
||||
const driverProfile = await driverService.getDriverProfile(currentDriverId);
|
||||
allSections.push({
|
||||
entityType: 'driver',
|
||||
entityId: currentDriverId,
|
||||
entityName: driver?.name ?? 'Your Profile',
|
||||
requests: driverResult.requests,
|
||||
entityName: driverProfile?.currentDriver?.name ?? 'Your Profile',
|
||||
requests: driverRequests,
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
// 2. Leagues where the user is admin/owner
|
||||
const allLeagues = await leagueRepo.findAll();
|
||||
const allLeagues = await leagueService.getAllLeagues();
|
||||
for (const league of allLeagues) {
|
||||
const membership = await leagueMembershipRepo.getMembership(league.id, currentDriverId);
|
||||
if (membership && isLeagueAdminOrHigherRole(membership.role)) {
|
||||
const membership = await leagueMembershipService.getMembership(league.id, currentDriverId);
|
||||
if (membership && LeagueRoleUtility.isLeagueAdminOrHigherRole(membership.role)) {
|
||||
// Load sponsorship requests for this league's active season
|
||||
try {
|
||||
// For simplicity, we'll query by season entityType - in production you'd get the active season ID
|
||||
const leagueResult = await sponsorshipService.getPendingSponsorshipRequests({
|
||||
const leagueRequests = await sponsorshipService.getPendingSponsorshipRequests({
|
||||
entityType: 'season',
|
||||
entityId: league.id, // Using league ID as a proxy for now
|
||||
});
|
||||
|
||||
if (leagueResult && leagueResult.requests.length > 0) {
|
||||
|
||||
if (leagueRequests.length > 0) {
|
||||
allSections.push({
|
||||
entityType: 'season',
|
||||
entityId: league.id,
|
||||
entityName: league.name,
|
||||
requests: leagueResult.requests,
|
||||
requests: leagueRequests,
|
||||
});
|
||||
}
|
||||
} catch (err) {
|
||||
@@ -84,28 +80,28 @@ export default function SponsorshipRequestsPage() {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// 3. Teams where the user is owner/manager
|
||||
const allTeams = await teamRepo.findAll();
|
||||
const allTeams = await teamService.getAllTeams();
|
||||
for (const team of allTeams) {
|
||||
const membership = await teamMembershipRepo.getMembership(team.id, currentDriverId);
|
||||
const membership = await teamService.getMembership(team.id, currentDriverId);
|
||||
if (membership && (membership.role === 'owner' || membership.role === 'manager')) {
|
||||
const teamResult = await sponsorshipService.getPendingSponsorshipRequests({
|
||||
const teamRequests = await sponsorshipService.getPendingSponsorshipRequests({
|
||||
entityType: 'team',
|
||||
entityId: team.id,
|
||||
});
|
||||
|
||||
if (teamResult && teamResult.requests.length > 0) {
|
||||
|
||||
if (teamRequests.length > 0) {
|
||||
allSections.push({
|
||||
entityType: 'team',
|
||||
entityId: team.id,
|
||||
entityName: team.name,
|
||||
requests: teamResult.requests,
|
||||
requests: teamRequests,
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
setSections(allSections);
|
||||
} catch (err) {
|
||||
console.error('Failed to load sponsorship requests:', err);
|
||||
@@ -120,24 +116,14 @@ export default function SponsorshipRequestsPage() {
|
||||
}, [loadAllRequests]);
|
||||
|
||||
const handleAccept = async (requestId: string) => {
|
||||
const useCase = getAcceptSponsorshipRequestUseCase();
|
||||
await useCase.execute({
|
||||
requestId,
|
||||
respondedBy: currentDriverId,
|
||||
});
|
||||
const { sponsorshipService } = useServices();
|
||||
await sponsorshipService.acceptSponsorshipRequest(requestId, currentDriverId);
|
||||
await loadAllRequests();
|
||||
};
|
||||
|
||||
const handleReject = async (requestId: string, reason?: string) => {
|
||||
const useCase = getRejectSponsorshipRequestUseCase();
|
||||
const input: { requestId: string; respondedBy: string; reason?: string } = {
|
||||
requestId,
|
||||
respondedBy: currentDriverId,
|
||||
};
|
||||
if (typeof reason === 'string') {
|
||||
input.reason = reason;
|
||||
}
|
||||
await useCase.execute(input);
|
||||
const { sponsorshipService } = useServices();
|
||||
await sponsorshipService.rejectSponsorshipRequest(requestId, currentDriverId, reason);
|
||||
await loadAllRequests();
|
||||
};
|
||||
|
||||
|
||||
@@ -50,4 +50,9 @@ export class DriversApiClient extends BaseApiClient {
|
||||
getDriverProfile(driverId: string): Promise<DriverProfileDTO> {
|
||||
return this.get<DriverProfileDTO>(`/drivers/${driverId}/profile`);
|
||||
}
|
||||
|
||||
/** Update current driver profile */
|
||||
updateProfile(updates: { bio?: string; country?: string }): Promise<DriverDTO> {
|
||||
return this.put<DriverDTO>('/drivers/profile', updates);
|
||||
}
|
||||
}
|
||||
@@ -50,6 +50,11 @@ export class LeaguesApiClient extends BaseApiClient {
|
||||
return this.patch<{ success: boolean }>(`/leagues/${leagueId}/members/${targetDriverId}/remove`, { performerDriverId });
|
||||
}
|
||||
|
||||
/** Update a member's role in league */
|
||||
updateMemberRole(leagueId: string, performerDriverId: string, targetDriverId: string, newRole: string): Promise<{ success: boolean }> {
|
||||
return this.patch<{ success: boolean }>(`/leagues/${leagueId}/members/${targetDriverId}/role`, { performerDriverId, newRole });
|
||||
}
|
||||
|
||||
/** Get league seasons */
|
||||
getSeasons(leagueId: string): Promise<{ seasons: Array<{ id: string; status: string }> }> {
|
||||
return this.get<{ seasons: Array<{ id: string; status: string }> }>(`/leagues/${leagueId}/seasons`);
|
||||
@@ -77,4 +82,9 @@ export class LeaguesApiClient extends BaseApiClient {
|
||||
newOwnerId,
|
||||
});
|
||||
}
|
||||
|
||||
/** Get races for a league */
|
||||
getRaces(leagueId: string): Promise<{ races: any[] }> {
|
||||
return this.get<{ races: any[] }>(`/leagues/${leagueId}/races`);
|
||||
}
|
||||
}
|
||||
18
apps/website/lib/api/penalties/PenaltiesApiClient.ts
Normal file
18
apps/website/lib/api/penalties/PenaltiesApiClient.ts
Normal file
@@ -0,0 +1,18 @@
|
||||
import { BaseApiClient } from '../base/BaseApiClient';
|
||||
|
||||
/**
|
||||
* Penalties API Client
|
||||
*
|
||||
* Handles all penalty-related API operations.
|
||||
*/
|
||||
export class PenaltiesApiClient extends BaseApiClient {
|
||||
/** Get penalties for a race */
|
||||
getRacePenalties(raceId: string): Promise<{ penalties: any[] }> {
|
||||
return this.get<{ penalties: any[] }>(`/races/${raceId}/penalties`);
|
||||
}
|
||||
|
||||
/** Apply a penalty */
|
||||
applyPenalty(input: any): Promise<void> {
|
||||
return this.post<void>('/races/penalties/apply', input);
|
||||
}
|
||||
}
|
||||
@@ -30,4 +30,14 @@ export class ProtestsApiClient extends BaseApiClient {
|
||||
requestDefense(input: RequestProtestDefenseCommandDTO): Promise<void> {
|
||||
return this.post<void>('/races/protests/defense/request', input);
|
||||
}
|
||||
|
||||
/** Review protest */
|
||||
reviewProtest(input: { protestId: string; stewardId: string; decision: string; decisionNotes: string }): Promise<void> {
|
||||
return this.post<void>(`/protests/${input.protestId}/review`, input);
|
||||
}
|
||||
|
||||
/** Get protests for a race */
|
||||
getRaceProtests(raceId: string): Promise<{ protests: any[] }> {
|
||||
return this.get<{ protests: any[] }>(`/races/${raceId}/protests`);
|
||||
}
|
||||
}
|
||||
@@ -44,4 +44,19 @@ export class SponsorsApiClient extends BaseApiClient {
|
||||
getSponsor(sponsorId: string): Promise<SponsorDTO | null> {
|
||||
return this.get<SponsorDTO | null>(`/sponsors/${sponsorId}`);
|
||||
}
|
||||
|
||||
/** Get pending sponsorship requests for an entity */
|
||||
getPendingSponsorshipRequests(params: { entityType: string; entityId: string }): Promise<{ requests: any[] }> {
|
||||
return this.get<{ requests: any[] }>(`/sponsors/requests?entityType=${params.entityType}&entityId=${params.entityId}`);
|
||||
}
|
||||
|
||||
/** Accept a sponsorship request */
|
||||
acceptSponsorshipRequest(requestId: string, respondedBy: string): Promise<void> {
|
||||
return this.post(`/sponsors/requests/${requestId}/accept`, { respondedBy });
|
||||
}
|
||||
|
||||
/** Reject a sponsorship request */
|
||||
rejectSponsorshipRequest(requestId: string, respondedBy: string, reason?: string): Promise<void> {
|
||||
return this.post(`/sponsors/requests/${requestId}/reject`, { respondedBy, reason });
|
||||
}
|
||||
}
|
||||
@@ -9,6 +9,8 @@ import { AnalyticsApiClient } from '../api/analytics/AnalyticsApiClient';
|
||||
import { MediaApiClient } from '../api/media/MediaApiClient';
|
||||
import { DashboardApiClient } from '../api/dashboard/DashboardApiClient';
|
||||
import { ProtestsApiClient } from '../api/protests/ProtestsApiClient';
|
||||
import { PenaltiesApiClient } from '../api/penalties/PenaltiesApiClient';
|
||||
import { PenaltyService } from './penalties/PenaltyService';
|
||||
import { ConsoleErrorReporter } from '../infrastructure/logging/ConsoleErrorReporter';
|
||||
import { ConsoleLogger } from '../infrastructure/logging/ConsoleLogger';
|
||||
|
||||
@@ -59,6 +61,7 @@ export class ServiceFactory {
|
||||
media: MediaApiClient;
|
||||
dashboard: DashboardApiClient;
|
||||
protests: ProtestsApiClient;
|
||||
penalties: PenaltiesApiClient;
|
||||
};
|
||||
|
||||
constructor(baseUrl: string) {
|
||||
@@ -75,6 +78,7 @@ export class ServiceFactory {
|
||||
media: new MediaApiClient(baseUrl, this.errorReporter, this.logger),
|
||||
dashboard: new DashboardApiClient(baseUrl, this.errorReporter, this.logger),
|
||||
protests: new ProtestsApiClient(baseUrl, this.errorReporter, this.logger),
|
||||
penalties: new PenaltiesApiClient(baseUrl, this.errorReporter, this.logger),
|
||||
};
|
||||
}
|
||||
|
||||
@@ -231,4 +235,11 @@ export class ServiceFactory {
|
||||
createProtestService(): ProtestService {
|
||||
return new ProtestService(this.apiClients.protests);
|
||||
}
|
||||
|
||||
/**
|
||||
* Create PenaltyService instance
|
||||
*/
|
||||
createPenaltyService(): PenaltyService {
|
||||
return new PenaltyService(this.apiClients.penalties);
|
||||
}
|
||||
}
|
||||
@@ -25,6 +25,7 @@ import { MembershipFeeService } from './payments/MembershipFeeService';
|
||||
import { AuthService } from './auth/AuthService';
|
||||
import { SessionService } from './auth/SessionService';
|
||||
import { ProtestService } from './protests/ProtestService';
|
||||
import { PenaltyService } from './penalties/PenaltyService';
|
||||
|
||||
export interface Services {
|
||||
raceService: RaceService;
|
||||
@@ -48,6 +49,7 @@ export interface Services {
|
||||
authService: AuthService;
|
||||
sessionService: SessionService;
|
||||
protestService: ProtestService;
|
||||
penaltyService: PenaltyService;
|
||||
}
|
||||
|
||||
const ServicesContext = createContext<Services | null>(null);
|
||||
@@ -82,6 +84,7 @@ export function ServiceProvider({ children }: ServiceProviderProps) {
|
||||
authService: serviceFactory.createAuthService(),
|
||||
sessionService: serviceFactory.createSessionService(),
|
||||
protestService: serviceFactory.createProtestService(),
|
||||
penaltyService: serviceFactory.createPenaltyService(),
|
||||
};
|
||||
}, []);
|
||||
|
||||
|
||||
@@ -54,10 +54,34 @@ export class DriverService {
|
||||
}
|
||||
|
||||
/**
|
||||
* Get driver profile with full details and view model transformation
|
||||
*/
|
||||
* Get driver profile with full details and view model transformation
|
||||
*/
|
||||
async getDriverProfile(driverId: string): Promise<DriverProfileViewModel> {
|
||||
const dto = await this.apiClient.getDriverProfile(driverId);
|
||||
return new DriverProfileViewModel(dto);
|
||||
}
|
||||
|
||||
/**
|
||||
* Update current driver profile with view model transformation
|
||||
*/
|
||||
async updateProfile(updates: { bio?: string; country?: string }): Promise<DriverProfileViewModel> {
|
||||
const dto = await this.apiClient.updateProfile(updates);
|
||||
// After updating, get the full profile again to return updated view model
|
||||
return this.getDriverProfile(dto.id);
|
||||
}
|
||||
|
||||
/**
|
||||
* Find driver by ID
|
||||
*/
|
||||
async findById(id: string): Promise<DriverDTO | null> {
|
||||
return this.apiClient.getDriver(id);
|
||||
}
|
||||
|
||||
/**
|
||||
* Find multiple drivers by IDs
|
||||
*/
|
||||
async findByIds(ids: string[]): Promise<DriverDTO[]> {
|
||||
const drivers = await Promise.all(ids.map(id => this.apiClient.getDriver(id)));
|
||||
return drivers.filter((d): d is DriverDTO => d !== null);
|
||||
}
|
||||
}
|
||||
@@ -13,6 +13,7 @@ import { LeagueSummaryViewModel } from "@/lib/view-models/LeagueSummaryViewModel
|
||||
import { RemoveMemberViewModel } from "@/lib/view-models/RemoveMemberViewModel";
|
||||
import { LeagueDetailViewModel } from "@/lib/view-models/LeagueDetailViewModel";
|
||||
import { LeagueDetailPageViewModel, SponsorInfo } from "@/lib/view-models/LeagueDetailPageViewModel";
|
||||
import { RaceViewModel } from "@/lib/view-models/RaceViewModel";
|
||||
import { SubmitBlocker, ThrottleBlocker } from "@/lib/blockers";
|
||||
import { DriverDTO } from "@/lib/types/DriverDTO";
|
||||
import { RaceDTO } from "@/lib/types/generated/RaceDTO";
|
||||
@@ -107,6 +108,13 @@ export class LeagueService {
|
||||
return new RemoveMemberViewModel(dto);
|
||||
}
|
||||
|
||||
/**
|
||||
* Update a member's role in league
|
||||
*/
|
||||
async updateMemberRole(leagueId: string, performerDriverId: string, targetDriverId: string, newRole: string): Promise<{ success: boolean }> {
|
||||
return this.apiClient.updateMemberRole(leagueId, performerDriverId, targetDriverId, newRole);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get league detail with owner, membership, and sponsor info
|
||||
*/
|
||||
@@ -192,7 +200,7 @@ export class LeagueService {
|
||||
const memberships = await this.apiClient.getMemberships(leagueId);
|
||||
|
||||
// Get all races for this league - TODO: implement API endpoint
|
||||
const allRaces: RaceDTO[] = []; // TODO: fetch from API
|
||||
const allRaces: RaceViewModel[] = []; // TODO: fetch from API and map to RaceViewModel
|
||||
|
||||
// Get league stats
|
||||
const leagueStats = await this.apiClient.getTotal(); // TODO: get stats for specific league
|
||||
|
||||
@@ -57,12 +57,16 @@ export class LeagueSettingsService {
|
||||
|
||||
// Get members
|
||||
const membershipsDto = await this.leaguesApiClient.getMemberships(leagueId);
|
||||
const members: DriverDTO[] = [];
|
||||
const members: DriverSummaryViewModel[] = [];
|
||||
for (const member of membershipsDto.members) {
|
||||
if (member.driverId !== league.ownerId && member.role !== 'owner') {
|
||||
const driver = await this.driversApiClient.getDriver(member.driverId);
|
||||
if (driver) {
|
||||
members.push(driver);
|
||||
members.push(new DriverSummaryViewModel({
|
||||
driver,
|
||||
rating: driver.rating ?? null,
|
||||
rank: null, // TODO: get from API
|
||||
}));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -47,4 +47,11 @@ export class MediaService {
|
||||
getTeamLogo(teamId: string): string {
|
||||
return `/api/media/teams/${teamId}/logo`;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get driver avatar URL
|
||||
*/
|
||||
getDriverAvatar(driverId: string): string {
|
||||
return `/api/media/avatar/${driverId}`;
|
||||
}
|
||||
}
|
||||
28
apps/website/lib/services/penalties/PenaltyService.ts
Normal file
28
apps/website/lib/services/penalties/PenaltyService.ts
Normal file
@@ -0,0 +1,28 @@
|
||||
import { PenaltiesApiClient } from '../../api/penalties/PenaltiesApiClient';
|
||||
|
||||
/**
|
||||
* Penalty Service
|
||||
*
|
||||
* Orchestrates penalty operations by coordinating API calls and view model creation.
|
||||
* All dependencies are injected via constructor.
|
||||
*/
|
||||
export class PenaltyService {
|
||||
constructor(
|
||||
private readonly apiClient: PenaltiesApiClient
|
||||
) {}
|
||||
|
||||
/**
|
||||
* Find penalties by race ID
|
||||
*/
|
||||
async findByRaceId(raceId: string): Promise<any[]> {
|
||||
const dto = await this.apiClient.getRacePenalties(raceId);
|
||||
return dto.penalties;
|
||||
}
|
||||
|
||||
/**
|
||||
* Apply a penalty
|
||||
*/
|
||||
async applyPenalty(input: any): Promise<void> {
|
||||
await this.apiClient.applyPenalty(input);
|
||||
}
|
||||
}
|
||||
@@ -1,5 +1,7 @@
|
||||
import { ProtestsApiClient } from '../../api/protests/ProtestsApiClient';
|
||||
import { ProtestViewModel } from '../../view-models/ProtestViewModel';
|
||||
import { RaceViewModel } from '../../view-models/RaceViewModel';
|
||||
import { ProtestDriverViewModel } from '../../view-models/ProtestDriverViewModel';
|
||||
import type { LeagueAdminProtestsDTO, ApplyPenaltyCommandDTO, RequestProtestDefenseCommandDTO, DriverSummaryDTO } from '../../types';
|
||||
|
||||
/**
|
||||
@@ -34,9 +36,9 @@ export class ProtestService {
|
||||
*/
|
||||
async getProtestById(leagueId: string, protestId: string): Promise<{
|
||||
protest: ProtestViewModel;
|
||||
race: LeagueAdminProtestsDTO['racesById'][string];
|
||||
protestingDriver: DriverSummaryDTO;
|
||||
accusedDriver: DriverSummaryDTO;
|
||||
race: RaceViewModel;
|
||||
protestingDriver: ProtestDriverViewModel;
|
||||
accusedDriver: ProtestDriverViewModel;
|
||||
} | null> {
|
||||
const dto = await this.apiClient.getLeagueProtest(leagueId, protestId);
|
||||
const protest = dto.protests[0];
|
||||
@@ -48,9 +50,9 @@ export class ProtestService {
|
||||
|
||||
return {
|
||||
protest: new ProtestViewModel(protest),
|
||||
race,
|
||||
protestingDriver,
|
||||
accusedDriver,
|
||||
race: new RaceViewModel(race),
|
||||
protestingDriver: new ProtestDriverViewModel(protestingDriver),
|
||||
accusedDriver: new ProtestDriverViewModel(accusedDriver),
|
||||
};
|
||||
}
|
||||
|
||||
@@ -67,4 +69,19 @@ export class ProtestService {
|
||||
async requestDefense(input: RequestProtestDefenseCommandDTO): Promise<void> {
|
||||
await this.apiClient.requestDefense(input);
|
||||
}
|
||||
|
||||
/**
|
||||
* Review protest
|
||||
*/
|
||||
async reviewProtest(input: { protestId: string; stewardId: string; decision: string; decisionNotes: string }): Promise<void> {
|
||||
await this.apiClient.reviewProtest(input);
|
||||
}
|
||||
|
||||
/**
|
||||
* Find protests by race ID
|
||||
*/
|
||||
async findByRaceId(raceId: string): Promise<any[]> {
|
||||
const dto = await this.apiClient.getRaceProtests(raceId);
|
||||
return dto.protests;
|
||||
}
|
||||
}
|
||||
@@ -83,35 +83,45 @@ export class RaceService {
|
||||
}
|
||||
|
||||
/**
|
||||
* Transform API races page data to view model format
|
||||
* Transform API races page data to view model format
|
||||
*/
|
||||
private transformRacesPageData(dto: RacesPageDataDto): {
|
||||
upcomingRaces: Array<{ id: string; title: string; scheduledTime: string; status: string }>;
|
||||
completedRaces: Array<{ id: string; title: string; scheduledTime: string; status: string }>;
|
||||
totalCount: number;
|
||||
} {
|
||||
const upcomingRaces = dto.races
|
||||
.filter(race => race.status !== 'completed')
|
||||
.map(race => ({
|
||||
id: race.id,
|
||||
title: `${race.track} - ${race.car}`,
|
||||
scheduledTime: race.scheduledAt,
|
||||
status: race.status,
|
||||
}));
|
||||
|
||||
const completedRaces = dto.races
|
||||
.filter(race => race.status === 'completed')
|
||||
.map(race => ({
|
||||
id: race.id,
|
||||
title: `${race.track} - ${race.car}`,
|
||||
scheduledTime: race.scheduledAt,
|
||||
status: race.status,
|
||||
}));
|
||||
|
||||
return {
|
||||
upcomingRaces,
|
||||
completedRaces,
|
||||
totalCount: dto.races.length,
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Find races by league ID
|
||||
*/
|
||||
private transformRacesPageData(dto: RacesPageDataDto): {
|
||||
upcomingRaces: Array<{ id: string; title: string; scheduledTime: string; status: string }>;
|
||||
completedRaces: Array<{ id: string; title: string; scheduledTime: string; status: string }>;
|
||||
totalCount: number;
|
||||
} {
|
||||
const upcomingRaces = dto.races
|
||||
.filter(race => race.status !== 'completed')
|
||||
.map(race => ({
|
||||
id: race.id,
|
||||
title: `${race.track} - ${race.car}`,
|
||||
scheduledTime: race.scheduledAt,
|
||||
status: race.status,
|
||||
}));
|
||||
|
||||
const completedRaces = dto.races
|
||||
.filter(race => race.status === 'completed')
|
||||
.map(race => ({
|
||||
id: race.id,
|
||||
title: `${race.track} - ${race.car}`,
|
||||
scheduledTime: race.scheduledAt,
|
||||
status: race.status,
|
||||
}));
|
||||
|
||||
return {
|
||||
upcomingRaces,
|
||||
completedRaces,
|
||||
totalCount: dto.races.length,
|
||||
};
|
||||
async findByLeagueId(leagueId: string): Promise<any[]> {
|
||||
// Assuming the API has /races?leagueId=...
|
||||
// TODO: Update when API is implemented
|
||||
const dto = await this.apiClient.get('/races?leagueId=' + leagueId) as { races: any[] };
|
||||
return dto.races;
|
||||
}
|
||||
}
|
||||
@@ -2,7 +2,8 @@ import type { SponsorsApiClient } from '../../api/sponsors/SponsorsApiClient';
|
||||
import type { GetEntitySponsorshipPricingResultDto } from '../../api/sponsors/SponsorsApiClient';
|
||||
import {
|
||||
SponsorshipPricingViewModel,
|
||||
SponsorSponsorshipsViewModel
|
||||
SponsorSponsorshipsViewModel,
|
||||
SponsorshipRequestViewModel
|
||||
} from '../../view-models';
|
||||
import type { SponsorSponsorshipsDTO } from '../../types/generated';
|
||||
|
||||
@@ -39,9 +40,22 @@ export class SponsorshipService {
|
||||
/**
|
||||
* Get pending sponsorship requests for an entity
|
||||
*/
|
||||
async getPendingSponsorshipRequests(params: { entityType: string; entityId: string }): Promise<{ requests: any[] }> {
|
||||
// TODO: Implement API call
|
||||
// For now, return empty
|
||||
return { requests: [] };
|
||||
async getPendingSponsorshipRequests(params: { entityType: string; entityId: string }): Promise<SponsorshipRequestViewModel[]> {
|
||||
const dto = await this.apiClient.getPendingSponsorshipRequests(params);
|
||||
return dto.requests.map(dto => new SponsorshipRequestViewModel(dto));
|
||||
}
|
||||
|
||||
/**
|
||||
* Accept a sponsorship request
|
||||
*/
|
||||
async acceptSponsorshipRequest(requestId: string, respondedBy: string): Promise<void> {
|
||||
await this.apiClient.acceptSponsorshipRequest(requestId, respondedBy);
|
||||
}
|
||||
|
||||
/**
|
||||
* Reject a sponsorship request
|
||||
*/
|
||||
async rejectSponsorshipRequest(requestId: string, respondedBy: string, reason?: string): Promise<void> {
|
||||
await this.apiClient.rejectSponsorshipRequest(requestId, respondedBy, reason);
|
||||
}
|
||||
}
|
||||
@@ -6,6 +6,7 @@ import { LeagueStandingsDTO } from '../types/generated/LeagueStandingsDTO';
|
||||
import { DriverDTO } from '../types/DriverDTO';
|
||||
import { RaceDTO } from '../types/generated/RaceDTO';
|
||||
import { LeagueScoringConfigDTO } from '../types/LeagueScoringConfigDTO';
|
||||
import { RaceViewModel } from './RaceViewModel';
|
||||
|
||||
// Sponsor info type
|
||||
export interface SponsorInfo {
|
||||
@@ -59,8 +60,8 @@ export class LeagueDetailPageViewModel {
|
||||
memberships: LeagueMembershipWithRole[];
|
||||
|
||||
// Races
|
||||
allRaces: RaceDTO[];
|
||||
runningRaces: RaceDTO[];
|
||||
allRaces: RaceViewModel[];
|
||||
runningRaces: RaceViewModel[];
|
||||
|
||||
// Stats
|
||||
averageSOF: number | null;
|
||||
@@ -96,7 +97,7 @@ export class LeagueDetailPageViewModel {
|
||||
scoringConfig: LeagueScoringConfigDTO | null,
|
||||
drivers: DriverDTO[],
|
||||
memberships: LeagueMembershipsDTO,
|
||||
allRaces: RaceDTO[],
|
||||
allRaces: RaceViewModel[],
|
||||
leagueStats: LeagueStatsDTO,
|
||||
sponsors: SponsorInfo[]
|
||||
) {
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
import type { LeagueConfigFormModel } from '../types/LeagueConfigFormModel';
|
||||
import type { LeagueScoringPresetDTO } from '../types/LeagueScoringPresetDTO';
|
||||
import type { DriverDTO } from '../types/DriverDTO';
|
||||
import { LeagueScoringPresetsViewModel } from './LeagueScoringPresetsViewModel';
|
||||
import { DriverSummaryViewModel } from './DriverSummaryViewModel';
|
||||
|
||||
@@ -17,7 +16,7 @@ export class LeagueSettingsViewModel {
|
||||
config: LeagueConfigFormModel;
|
||||
presets: LeagueScoringPresetDTO[];
|
||||
owner: DriverSummaryViewModel | null;
|
||||
members: DriverDTO[];
|
||||
members: DriverSummaryViewModel[];
|
||||
|
||||
constructor(dto: {
|
||||
league: {
|
||||
@@ -28,7 +27,7 @@ export class LeagueSettingsViewModel {
|
||||
config: LeagueConfigFormModel;
|
||||
presets: LeagueScoringPresetDTO[];
|
||||
owner: DriverSummaryViewModel | null;
|
||||
members: DriverDTO[];
|
||||
members: DriverSummaryViewModel[];
|
||||
}) {
|
||||
this.league = dto.league;
|
||||
this.config = dto.config;
|
||||
|
||||
@@ -1,20 +1,21 @@
|
||||
import { LeagueStandingDTO } from '../types/generated/LeagueStandingDTO';
|
||||
import { StandingEntryViewModel } from './StandingEntryViewModel';
|
||||
import { DriverDTO } from '../types/DriverDTO';
|
||||
import { LeagueMembership } from '../types/LeagueMembership';
|
||||
|
||||
export class LeagueStandingsViewModel {
|
||||
standings: StandingEntryViewModel[];
|
||||
drivers: DriverDTO[];
|
||||
memberships: LeagueMembership[];
|
||||
|
||||
constructor(dto: { standings: LeagueStandingDTO[] }, currentUserId: string, previousStandings?: LeagueStandingDTO[]) {
|
||||
constructor(dto: { standings: LeagueStandingDTO[]; drivers: DriverDTO[]; memberships: LeagueMembership[] }, currentUserId: string, previousStandings?: LeagueStandingDTO[]) {
|
||||
const leaderPoints = dto.standings[0]?.points || 0;
|
||||
this.standings = dto.standings.map((entry, index) => {
|
||||
const nextPoints = dto.standings[index + 1]?.points || entry.points;
|
||||
const previousPosition = previousStandings?.find(p => p.driverId === entry.driverId)?.position;
|
||||
return new StandingEntryViewModel(entry, leaderPoints, nextPoints, currentUserId, previousPosition);
|
||||
});
|
||||
this.drivers = dto.drivers;
|
||||
this.memberships = dto.memberships;
|
||||
}
|
||||
|
||||
// Note: The generated DTO doesn't have these fields
|
||||
// These will need to be added when the OpenAPI spec is updated
|
||||
drivers: any[] = [];
|
||||
memberships: any[] = [];
|
||||
}
|
||||
13
apps/website/lib/view-models/ProtestDriverViewModel.ts
Normal file
13
apps/website/lib/view-models/ProtestDriverViewModel.ts
Normal file
@@ -0,0 +1,13 @@
|
||||
import { DriverSummaryDTO } from '../types/generated/LeagueAdminProtestsDTO';
|
||||
|
||||
export class ProtestDriverViewModel {
|
||||
constructor(private readonly dto: DriverSummaryDTO) {}
|
||||
|
||||
get id(): string {
|
||||
return this.dto.id;
|
||||
}
|
||||
|
||||
get name(): string {
|
||||
return this.dto.name;
|
||||
}
|
||||
}
|
||||
@@ -11,6 +11,9 @@ export class ProtestViewModel {
|
||||
accusedDriverId: string;
|
||||
description: string;
|
||||
submittedAt: string;
|
||||
status: string;
|
||||
reviewedAt?: string;
|
||||
decisionNotes?: string;
|
||||
|
||||
constructor(dto: ProtestDTO) {
|
||||
this.id = dto.id;
|
||||
@@ -19,6 +22,10 @@ export class ProtestViewModel {
|
||||
this.accusedDriverId = dto.accusedDriverId;
|
||||
this.description = dto.description;
|
||||
this.submittedAt = dto.submittedAt;
|
||||
// TODO: Add these fields to DTO when available
|
||||
this.status = 'pending';
|
||||
this.reviewedAt = undefined;
|
||||
this.decisionNotes = undefined;
|
||||
}
|
||||
|
||||
/** UI-specific: Formatted submitted date */
|
||||
@@ -26,8 +33,8 @@ export class ProtestViewModel {
|
||||
return new Date(this.submittedAt).toLocaleString();
|
||||
}
|
||||
|
||||
/** UI-specific: Status display - placeholder since status not in current DTO */
|
||||
/** UI-specific: Status display */
|
||||
get statusDisplay(): string {
|
||||
return 'Pending'; // TODO: Update when status is added to DTO
|
||||
return 'Pending';
|
||||
}
|
||||
}
|
||||
34
apps/website/lib/view-models/RaceViewModel.ts
Normal file
34
apps/website/lib/view-models/RaceViewModel.ts
Normal file
@@ -0,0 +1,34 @@
|
||||
import { RaceDTO } from '../types/generated/RaceDTO';
|
||||
|
||||
export class RaceViewModel {
|
||||
constructor(private readonly dto: RaceDTO, private readonly _status?: string, private readonly _registeredCount?: number, private readonly _strengthOfField?: number) {}
|
||||
|
||||
get id(): string {
|
||||
return this.dto.id;
|
||||
}
|
||||
|
||||
get name(): string {
|
||||
return this.dto.name;
|
||||
}
|
||||
|
||||
get date(): string {
|
||||
return this.dto.date;
|
||||
}
|
||||
|
||||
get status(): string | undefined {
|
||||
return this._status;
|
||||
}
|
||||
|
||||
get registeredCount(): number | undefined {
|
||||
return this._registeredCount;
|
||||
}
|
||||
|
||||
get strengthOfField(): number | undefined {
|
||||
return this._strengthOfField;
|
||||
}
|
||||
|
||||
/** UI-specific: Formatted date */
|
||||
get formattedDate(): string {
|
||||
return new Date(this.date).toLocaleDateString();
|
||||
}
|
||||
}
|
||||
54
apps/website/lib/view-models/SponsorshipRequestViewModel.ts
Normal file
54
apps/website/lib/view-models/SponsorshipRequestViewModel.ts
Normal file
@@ -0,0 +1,54 @@
|
||||
import { PendingRequestDTO } from '@/components/sponsors/PendingSponsorshipRequests';
|
||||
|
||||
export class SponsorshipRequestViewModel {
|
||||
id: string;
|
||||
sponsorId: string;
|
||||
sponsorName: string;
|
||||
sponsorLogo?: string;
|
||||
tier: 'main' | 'secondary';
|
||||
offeredAmount: number;
|
||||
currency: string;
|
||||
formattedAmount: string;
|
||||
message?: string;
|
||||
createdAt: Date;
|
||||
platformFee: number;
|
||||
netAmount: number;
|
||||
|
||||
constructor(dto: PendingRequestDTO) {
|
||||
this.id = dto.id;
|
||||
this.sponsorId = dto.sponsorId;
|
||||
this.sponsorName = dto.sponsorName;
|
||||
this.sponsorLogo = dto.sponsorLogo;
|
||||
this.tier = dto.tier;
|
||||
this.offeredAmount = dto.offeredAmount;
|
||||
this.currency = dto.currency;
|
||||
this.formattedAmount = dto.formattedAmount;
|
||||
this.message = dto.message;
|
||||
this.createdAt = dto.createdAt;
|
||||
this.platformFee = dto.platformFee;
|
||||
this.netAmount = dto.netAmount;
|
||||
}
|
||||
|
||||
/** UI-specific: Formatted date */
|
||||
get formattedDate(): string {
|
||||
return this.createdAt.toLocaleDateString('en-US', {
|
||||
month: 'short',
|
||||
day: 'numeric',
|
||||
});
|
||||
}
|
||||
|
||||
/** UI-specific: Net amount in dollars */
|
||||
get netAmountDollars(): string {
|
||||
return `$${(this.netAmount / 100).toFixed(2)}`;
|
||||
}
|
||||
|
||||
/** UI-specific: Tier display */
|
||||
get tierDisplay(): string {
|
||||
return this.tier === 'main' ? 'Main Sponsor' : 'Secondary';
|
||||
}
|
||||
|
||||
/** UI-specific: Tier badge variant */
|
||||
get tierBadgeVariant(): 'primary' | 'secondary' {
|
||||
return this.tier === 'main' ? 'primary' : 'secondary';
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user