This commit is contained in:
2025-12-11 00:57:32 +01:00
parent 1303a14493
commit 6a427eab57
112 changed files with 6148 additions and 2272 deletions

View File

@@ -3,14 +3,22 @@
import { useState, useEffect } from 'react';
import Card from '@/components/ui/Card';
import DriverIdentity from '@/components/drivers/DriverIdentity';
import { getDriverRepository, getDriverStats } from '@/lib/di-container';
import { EntityMappers } from '@gridpilot/racing/application/mappers/EntityMappers';
import type { DriverDTO } from '@gridpilot/racing/application/dto/DriverDTO';
import type { TeamMembership, TeamRole } from '@gridpilot/racing';
import {
getTeamRosterViewModel,
type TeamRosterViewModel,
} from '@/lib/presenters/TeamRosterPresenter';
type TeamRole = 'owner' | 'manager' | 'driver';
interface TeamMembershipSummary {
driverId: string;
role: TeamRole;
joinedAt: Date;
}
interface TeamRosterProps {
teamId: string;
memberships: TeamMembership[];
memberships: TeamMembershipSummary[];
isAdmin: boolean;
onRemoveMember?: (driverId: string) => void;
onChangeRole?: (driverId: string, newRole: TeamRole) => void;
@@ -23,31 +31,22 @@ export default function TeamRoster({
onRemoveMember,
onChangeRole,
}: TeamRosterProps) {
const [drivers, setDrivers] = useState<Record<string, DriverDTO>>({});
const [viewModel, setViewModel] = useState<TeamRosterViewModel | null>(null);
const [loading, setLoading] = useState(true);
const [sortBy, setSortBy] = useState<'role' | 'rating' | 'name'>('rating');
useEffect(() => {
const loadDrivers = async () => {
const driverRepo = getDriverRepository();
const allDrivers = await driverRepo.findAll();
const driverMap: Record<string, DriverDTO> = {};
for (const membership of memberships) {
const driver = allDrivers.find((d) => d.id === membership.driverId);
if (driver) {
const dto = EntityMappers.toDriverDTO(driver);
if (dto) {
driverMap[membership.driverId] = dto;
}
}
const load = async () => {
setLoading(true);
try {
const vm = await getTeamRosterViewModel(memberships);
setViewModel(vm);
} finally {
setLoading(false);
}
setDrivers(driverMap);
setLoading(false);
};
void loadDrivers();
void load();
}, [memberships]);
const getRoleBadgeColor = (role: TeamRole) => {
@@ -65,36 +64,28 @@ export default function TeamRoster({
return role.charAt(0).toUpperCase() + role.slice(1);
};
const sortedMemberships = [...memberships].sort((a, b) => {
switch (sortBy) {
case 'rating': {
const statsA = getDriverStats(a.driverId);
const statsB = getDriverStats(b.driverId);
return (statsB?.rating || 0) - (statsA?.rating || 0);
}
case 'role': {
const roleOrder = { owner: 0, manager: 1, driver: 2 };
return roleOrder[a.role] - roleOrder[b.role];
}
case 'name': {
const driverA = drivers[a.driverId];
const driverB = drivers[b.driverId];
return (driverA?.name || '').localeCompare(driverB?.name || '');
}
default:
return 0;
}
});
const sortedMembers = viewModel
? [...viewModel.members].sort((a, b) => {
switch (sortBy) {
case 'rating': {
const ratingA = a.rating ?? 0;
const ratingB = b.rating ?? 0;
return ratingB - ratingA;
}
case 'role': {
const roleOrder: Record<TeamRole, number> = { owner: 0, manager: 1, driver: 2 };
return roleOrder[a.role] - roleOrder[b.role];
}
case 'name': {
return a.driver.name.localeCompare(b.driver.name);
}
default:
return 0;
}
})
: [];
const teamAverageRating =
memberships.length > 0
? Math.round(
memberships.reduce((sum, m) => {
const stats = getDriverStats(m.driverId);
return sum + (stats?.rating || 0);
}, 0) / memberships.length,
)
: 0;
const teamAverageRating = viewModel?.averageRating ?? 0;
if (loading) {
return (
@@ -130,43 +121,42 @@ export default function TeamRoster({
</div>
<div className="space-y-3">
{sortedMemberships.map((membership) => {
const driver = drivers[membership.driverId];
const driverStats = getDriverStats(membership.driverId);
if (!driver) return null;
{sortedMembers.map((member) => {
const { driver, role, joinedAt, rating, overallRank } = member;
const canManageMembership = isAdmin && membership.role !== 'owner';
const canManageMembership = isAdmin && role !== 'owner';
return (
<div
key={membership.driverId}
key={driver.id}
className="flex items-center justify-between p-4 rounded-lg bg-deep-graphite border border-charcoal-outline hover:border-charcoal-outline/60 transition-colors"
>
<DriverIdentity
driver={driver}
href={`/drivers/${driver.id}?from=team&teamId=${teamId}`}
contextLabel={getRoleLabel(membership.role)}
contextLabel={getRoleLabel(role)}
meta={
<span>
{driver.country} Joined{' '}
{new Date(membership.joinedAt).toLocaleDateString()}
{driver.country} Joined {new Date(joinedAt).toLocaleDateString()}
</span>
}
size="md"
/>
{driverStats && (
{rating !== null && (
<div className="flex items-center gap-6 text-center">
<div>
<div className="text-lg font-bold text-primary-blue">
{driverStats.rating}
{rating}
</div>
<div className="text-xs text-gray-400">Rating</div>
</div>
<div>
<div className="text-sm text-gray-300">#{driverStats.overallRank}</div>
<div className="text-xs text-gray-500">Rank</div>
</div>
{overallRank !== null && (
<div>
<div className="text-sm text-gray-300">#{overallRank}</div>
<div className="text-xs text-gray-500">Rank</div>
</div>
)}
</div>
)}
@@ -174,9 +164,9 @@ export default function TeamRoster({
<div className="flex items-center gap-2">
<select
className="px-3 py-2 bg-iron-gray border-0 rounded text-white ring-1 ring-inset ring-charcoal-outline focus:ring-2 focus:ring-primary-blue transition-all duration-150 text-sm"
value={membership.role}
value={role}
onChange={(e) =>
onChangeRole?.(membership.driverId, e.target.value as TeamRole)
onChangeRole?.(driver.id, e.target.value as TeamRole)
}
>
<option value="driver">Driver</option>
@@ -184,7 +174,7 @@ export default function TeamRoster({
</select>
<button
onClick={() => onRemoveMember?.(membership.driverId)}
onClick={() => onRemoveMember?.(driver.id)}
className="px-3 py-2 bg-danger-red/20 hover:bg-danger-red/30 text-danger-red rounded text-sm font-medium transition-colors"
>
Remove