This commit is contained in:
2025-12-11 11:25:22 +01:00
parent 6a427eab57
commit e4c1be628d
86 changed files with 1222 additions and 736 deletions

View File

@@ -4,24 +4,28 @@ import { useState, useEffect } from 'react';
import Card from '@/components/ui/Card';
import Button from '@/components/ui/Button';
import Input from '@/components/ui/Input';
import {
getDriverRepository,
getGetTeamJoinRequestsUseCase,
getApproveTeamJoinRequestUseCase,
getRejectTeamJoinRequestUseCase,
getUpdateTeamUseCase,
} from '@/lib/di-container';
import { EntityMappers } from '@gridpilot/racing/application/mappers/EntityMappers';
import type { DriverDTO } from '@gridpilot/racing/application/dto/DriverDTO';
import type { Team, TeamJoinRequest } from '@gridpilot/racing';
import {
loadTeamAdminViewModel,
approveTeamJoinRequestAndReload,
rejectTeamJoinRequestAndReload,
updateTeamDetails,
type TeamAdminJoinRequestViewModel,
} from '@/lib/presenters/TeamAdminPresenter';
interface TeamAdminProps {
team: Team;
team: {
id: string;
name: string;
tag: string;
description: string;
ownerId: string;
};
onUpdate: () => void;
}
export default function TeamAdmin({ team, onUpdate }: TeamAdminProps) {
const [joinRequests, setJoinRequests] = useState<TeamJoinRequest[]>([]);
const [joinRequests, setJoinRequests] = useState<TeamAdminJoinRequestViewModel[]>([]);
const [requestDrivers, setRequestDrivers] = useState<Record<string, DriverDTO>>({});
const [loading, setLoading] = useState(true);
const [editMode, setEditMode] = useState(false);
@@ -32,38 +36,38 @@ export default function TeamAdmin({ team, onUpdate }: TeamAdminProps) {
});
useEffect(() => {
void loadJoinRequests();
}, [team.id]);
const load = async () => {
setLoading(true);
try {
const viewModel = await loadTeamAdminViewModel(team as any);
setJoinRequests(viewModel.requests);
const loadJoinRequests = async () => {
const useCase = getGetTeamJoinRequestsUseCase();
await useCase.execute({ teamId: team.id });
const viewModel = useCase.presenter.getViewModel();
setJoinRequests(viewModel.requests);
const driverRepo = getDriverRepository();
const allDrivers = await driverRepo.findAll();
const driverMap: Record<string, DriverDTO> = {};
for (const request of viewModel.requests) {
const driver = allDrivers.find(d => d.id === request.driverId);
if (driver) {
const dto = EntityMappers.toDriverDTO(driver);
if (dto) {
driverMap[request.driverId] = dto;
const driversById: Record<string, DriverDTO> = {};
for (const request of viewModel.requests) {
if (request.driver) {
driversById[request.driverId] = request.driver;
}
}
setRequestDrivers(driversById);
} finally {
setLoading(false);
}
}
};
setRequestDrivers(driverMap);
setLoading(false);
};
void load();
}, [team.id, team.name, team.tag, team.description, team.ownerId]);
const handleApprove = async (requestId: string) => {
try {
const useCase = getApproveTeamJoinRequestUseCase();
await useCase.execute({ requestId });
await loadJoinRequests();
const updated = await approveTeamJoinRequestAndReload(requestId, team.id);
setJoinRequests(updated);
const driversById: Record<string, DriverDTO> = {};
for (const request of updated) {
if (request.driver) {
driversById[request.driverId] = request.driver;
}
}
setRequestDrivers(driversById);
onUpdate();
} catch (error) {
alert(error instanceof Error ? error.message : 'Failed to approve request');
@@ -72,9 +76,15 @@ export default function TeamAdmin({ team, onUpdate }: TeamAdminProps) {
const handleReject = async (requestId: string) => {
try {
const useCase = getRejectTeamJoinRequestUseCase();
await useCase.execute({ requestId });
await loadJoinRequests();
const updated = await rejectTeamJoinRequestAndReload(requestId, team.id);
setJoinRequests(updated);
const driversById: Record<string, DriverDTO> = {};
for (const request of updated) {
if (request.driver) {
driversById[request.driverId] = request.driver;
}
}
setRequestDrivers(driversById);
} catch (error) {
alert(error instanceof Error ? error.message : 'Failed to reject request');
}
@@ -82,15 +92,12 @@ export default function TeamAdmin({ team, onUpdate }: TeamAdminProps) {
const handleSaveChanges = async () => {
try {
const useCase = getUpdateTeamUseCase();
await useCase.execute({
await updateTeamDetails({
teamId: team.id,
updates: {
name: editedTeam.name,
tag: editedTeam.tag,
description: editedTeam.description,
},
updatedBy: team.ownerId,
name: editedTeam.name,
tag: editedTeam.tag,
description: editedTeam.description,
updatedByDriverId: team.ownerId,
});
setEditMode(false);
onUpdate();
@@ -194,7 +201,7 @@ export default function TeamAdmin({ team, onUpdate }: TeamAdminProps) {
) : joinRequests.length > 0 ? (
<div className="space-y-3">
{joinRequests.map((request) => {
const driver = requestDrivers[request.driverId];
const driver = requestDrivers[request.driverId] ?? request.driver;
if (!driver) return null;
return (

View File

@@ -2,85 +2,31 @@
import { useState, useEffect } from 'react';
import Card from '@/components/ui/Card';
import { getStandingRepository, getLeagueRepository, getTeamMembershipRepository } from '@/lib/di-container';
import { EntityMappers } from '@gridpilot/racing/application/mappers/EntityMappers';
import type { LeagueDTO } from '@gridpilot/racing/application/dto/LeagueDTO';
import {
loadTeamStandings,
type TeamLeagueStandingViewModel,
} from '@/lib/presenters/TeamStandingsPresenter';
interface TeamStandingsProps {
teamId: string;
leagues: string[];
}
interface TeamLeagueStanding {
leagueId: string;
leagueName: string;
position: number;
points: number;
wins: number;
racesCompleted: number;
}
export default function TeamStandings({ teamId, leagues }: TeamStandingsProps) {
const [standings, setStandings] = useState<TeamLeagueStanding[]>([]);
const [standings, setStandings] = useState<TeamLeagueStandingViewModel[]>([]);
const [loading, setLoading] = useState(true);
useEffect(() => {
const loadStandings = async () => {
const standingRepo = getStandingRepository();
const leagueRepo = getLeagueRepository();
const teamMembershipRepo = getTeamMembershipRepository();
const members = await teamMembershipRepo.getTeamMembers(teamId);
const memberIds = members.map(m => m.driverId);
const teamStandings: TeamLeagueStanding[] = [];
for (const leagueId of leagues) {
const league = await leagueRepo.findById(leagueId);
if (!league) continue;
const leagueStandings = await standingRepo.findByLeagueId(leagueId);
// Calculate team points (sum of all team members)
let totalPoints = 0;
let totalWins = 0;
let totalRaces = 0;
for (const standing of leagueStandings) {
if (memberIds.includes(standing.driverId)) {
totalPoints += standing.points;
totalWins += standing.wins;
totalRaces = Math.max(totalRaces, standing.racesCompleted);
}
}
// Calculate team position (simplified - based on total points)
const allTeamPoints = leagueStandings
.filter(s => memberIds.includes(s.driverId))
.reduce((sum, s) => sum + s.points, 0);
const position = leagueStandings
.filter((_, idx, arr) => {
const teamPoints = arr
.filter(s => memberIds.includes(s.driverId))
.reduce((sum, s) => sum + s.points, 0);
return teamPoints > allTeamPoints;
}).length + 1;
teamStandings.push({
leagueId,
leagueName: league.name,
position,
points: totalPoints,
wins: totalWins,
racesCompleted: totalRaces,
});
const load = async () => {
try {
const viewModel = await loadTeamStandings(teamId, leagues);
setStandings(viewModel.standings);
} finally {
setLoading(false);
}
setStandings(teamStandings);
setLoading(false);
};
loadStandings();
void load();
}, [teamId, leagues]);
if (loading) {