172 lines
5.7 KiB
TypeScript
172 lines
5.7 KiB
TypeScript
'use client';
|
|
|
|
import { TeamRosterItem } from '@/components/teams/TeamRosterItem';
|
|
import { TeamRosterList } from '@/components/teams/TeamRosterList';
|
|
import { useTeamRoster } from "@/hooks/team/useTeamRoster";
|
|
import { routes } from '@/lib/routing/RouteConfig';
|
|
import { sortMembers } from '@/lib/utilities/roster-utils';
|
|
import type { DriverViewModel } from '@/lib/view-models/DriverViewModel';
|
|
import { Button } from '@/ui/Button';
|
|
import { Card } from '@/ui/Card';
|
|
import { MinimalEmptyState } from '@/ui/EmptyState';
|
|
import { Heading } from '@/ui/Heading';
|
|
import { Select } from '@/ui/Select';
|
|
import { Stack } from '@/ui/Stack';
|
|
import { Text } from '@/ui/Text';
|
|
import { useMemo, useState } from 'react';
|
|
|
|
import { DateFormatter } from '@/lib/formatters/DateFormatter';
|
|
import { MemberFormatter } from '@/lib/formatters/MemberFormatter';
|
|
import { RatingFormatter } from '@/lib/formatters/RatingFormatter';
|
|
|
|
export type TeamRole = 'owner' | 'admin' | 'member';
|
|
export type TeamMemberRole = 'owner' | 'manager' | 'member';
|
|
|
|
interface TeamRosterProps {
|
|
teamId: string;
|
|
memberships: Array<{
|
|
driverId: string;
|
|
driverName: string;
|
|
role: 'owner' | 'manager' | 'member';
|
|
joinedAt: string;
|
|
isActive: boolean;
|
|
avatarUrl: string;
|
|
}>;
|
|
isAdmin: boolean;
|
|
onRemoveMember?: (driverId: string) => void;
|
|
onChangeRole?: (driverId: string, newRole: TeamRole) => void;
|
|
}
|
|
|
|
interface TeamMember {
|
|
driver: {
|
|
id: string;
|
|
name: string;
|
|
country: string;
|
|
};
|
|
role: TeamMemberRole;
|
|
joinedAt: string;
|
|
rating: number | null;
|
|
overallRank: number | null;
|
|
}
|
|
|
|
export function TeamRoster({
|
|
teamId,
|
|
memberships,
|
|
isAdmin,
|
|
onRemoveMember,
|
|
onChangeRole,
|
|
}: TeamRosterProps) {
|
|
const [sortBy, setSortBy] = useState<'role' | 'rating' | 'name'>('rating');
|
|
|
|
// Use hook for data fetching
|
|
const { data: teamMembers = [], isLoading: loading } = useTeamRoster(memberships);
|
|
|
|
const getRoleLabel = useMemo(() => (role: TeamRole | TeamMemberRole) => {
|
|
// Convert manager to admin for display
|
|
const displayRole = role === 'manager' ? 'admin' : role;
|
|
return displayRole.charAt(0).toUpperCase() + displayRole.slice(1);
|
|
}, []);
|
|
|
|
const sortedMembers = useMemo(() => {
|
|
return sortMembers(teamMembers as unknown as TeamMember[], sortBy);
|
|
}, [teamMembers, sortBy]);
|
|
|
|
const teamAverageRatingLabel = useMemo(() => {
|
|
if (teamMembers.length === 0) return '—';
|
|
const avg = teamMembers.reduce((sum: number, m: { rating?: number | null }) => sum + (m.rating || 0), 0) / teamMembers.length;
|
|
return RatingFormatter.format(avg);
|
|
}, [teamMembers]);
|
|
|
|
if (loading) {
|
|
return (
|
|
<Card>
|
|
<Stack display="flex" justifyContent="center" py={8}>
|
|
<Text color="text-gray-400">Loading roster...</Text>
|
|
</Stack>
|
|
</Card>
|
|
);
|
|
}
|
|
|
|
return (
|
|
<Card>
|
|
<Stack direction="row" align="center" justify="between" mb={6} wrap gap={4}>
|
|
<Stack>
|
|
<Heading level={3}>Team Roster</Heading>
|
|
<Text size="sm" color="text-gray-400" block mt={1}>
|
|
{MemberFormatter.formatCount(memberships.length)} • Avg Rating:{' '}
|
|
<Text color="text-primary-blue" weight="medium">{teamAverageRatingLabel}</Text>
|
|
</Text>
|
|
</Stack>
|
|
|
|
<Stack direction="row" align="center" gap={2}>
|
|
<Text size="sm" color="text-gray-400">Sort by:</Text>
|
|
<Stack width="32">
|
|
<Select
|
|
value={sortBy}
|
|
onChange={(e) => setSortBy(e.target.value as typeof sortBy)}
|
|
options={[
|
|
{ value: 'rating', label: 'Rating' },
|
|
{ value: 'role', label: 'Role' },
|
|
{ value: 'name', label: 'Name' },
|
|
]}
|
|
/>
|
|
</Stack>
|
|
</Stack>
|
|
</Stack>
|
|
|
|
{sortedMembers.length > 0 ? (
|
|
<TeamRosterList>
|
|
{sortedMembers.map((member) => {
|
|
const { driver, role, joinedAt, rating, overallRank } = member;
|
|
|
|
// Convert manager to admin for display purposes
|
|
const displayRole: TeamRole = role === 'manager' ? 'admin' : (role as TeamRole);
|
|
const canManageMembership = isAdmin && role !== 'owner';
|
|
|
|
return (
|
|
<TeamRosterItem
|
|
key={driver.id}
|
|
driver={driver as DriverViewModel}
|
|
href={`${routes.driver.detail(driver.id)}?from=team&teamId=${teamId}`}
|
|
roleLabel={getRoleLabel(role)}
|
|
joinedAtLabel={DateFormatter.formatShort(joinedAt)}
|
|
ratingLabel={RatingFormatter.format(rating)}
|
|
overallRankLabel={overallRank !== null ? `#${overallRank}` : null}
|
|
actions={canManageMembership ? (
|
|
<>
|
|
<Stack width="32">
|
|
<Select
|
|
value={displayRole}
|
|
onChange={(e) =>
|
|
onChangeRole?.(driver.id, e.target.value as TeamRole)
|
|
}
|
|
options={[
|
|
{ value: 'member', label: 'Member' },
|
|
{ value: 'admin', label: 'Admin' },
|
|
]}
|
|
/>
|
|
</Stack>
|
|
|
|
<Button
|
|
variant="danger"
|
|
size="sm"
|
|
onClick={() => onRemoveMember?.(driver.id)}
|
|
>
|
|
Remove
|
|
</Button>
|
|
</>
|
|
) : undefined}
|
|
/>
|
|
);
|
|
})}
|
|
</TeamRosterList>
|
|
) : (
|
|
<MinimalEmptyState
|
|
title="No team members yet"
|
|
description="When drivers join your team, they will appear here."
|
|
/>
|
|
)}
|
|
</Card>
|
|
);
|
|
}
|