165 lines
6.1 KiB
TypeScript
165 lines
6.1 KiB
TypeScript
import { Card } from '@/ui/Card';
|
|
import { Section } from '@/ui/Section';
|
|
import { Text } from '@/ui/Text';
|
|
import { Button } from '@/ui/Button';
|
|
import { Select } from '@/ui/Select';
|
|
import type { MembershipRole } from '@/lib/types/MembershipRole';
|
|
import type { LeagueRosterJoinRequestDTO } from '@/lib/types/generated/LeagueRosterJoinRequestDTO';
|
|
import type { LeagueRosterMemberDTO } from '@/lib/types/generated/LeagueRosterMemberDTO';
|
|
|
|
interface RosterAdminTemplateProps {
|
|
joinRequests: LeagueRosterJoinRequestDTO[];
|
|
members: LeagueRosterMemberDTO[];
|
|
loading: boolean;
|
|
pendingCountLabel: string;
|
|
onApprove: (requestId: string) => Promise<void>;
|
|
onReject: (requestId: string) => Promise<void>;
|
|
onRoleChange: (driverId: string, newRole: MembershipRole) => Promise<void>;
|
|
onRemove: (driverId: string) => Promise<void>;
|
|
roleOptions: MembershipRole[];
|
|
}
|
|
|
|
export function RosterAdminTemplate({
|
|
joinRequests,
|
|
members,
|
|
loading,
|
|
pendingCountLabel,
|
|
onApprove,
|
|
onReject,
|
|
onRoleChange,
|
|
onRemove,
|
|
roleOptions,
|
|
}: RosterAdminTemplateProps) {
|
|
return (
|
|
<Section>
|
|
<Card>
|
|
<Section>
|
|
<Section>
|
|
<Text size="2xl" weight="bold" className="text-white">
|
|
Roster Admin
|
|
</Text>
|
|
<Text size="sm" className="text-gray-400">
|
|
Manage join requests and member roles.
|
|
</Text>
|
|
</Section>
|
|
|
|
<Section>
|
|
<div className="flex items-center justify-between gap-3">
|
|
<Text size="lg" weight="semibold" className="text-white">
|
|
Pending join requests
|
|
</Text>
|
|
<Text size="xs" className="text-gray-500">
|
|
{pendingCountLabel}
|
|
</Text>
|
|
</div>
|
|
|
|
{loading ? (
|
|
<Text size="sm" className="text-gray-400">
|
|
Loading…
|
|
</Text>
|
|
) : joinRequests.length ? (
|
|
<div className="space-y-2">
|
|
{joinRequests.map((req) => (
|
|
<div
|
|
key={req.id}
|
|
className="flex items-center justify-between gap-3 bg-deep-graphite border border-charcoal-outline rounded p-3"
|
|
>
|
|
<div className="min-w-0">
|
|
<Text weight="medium" className="text-white truncate">
|
|
{(req.driver as any)?.name || 'Unknown'}
|
|
</Text>
|
|
<Text size="xs" className="text-gray-400 truncate">
|
|
{req.requestedAt}
|
|
</Text>
|
|
{req.message && (
|
|
<Text size="xs" className="text-gray-500 truncate">
|
|
{req.message}
|
|
</Text>
|
|
)}
|
|
</div>
|
|
|
|
<div className="flex items-center gap-2">
|
|
<Button
|
|
data-testid={`join-request-${req.id}-approve`}
|
|
onClick={() => onApprove(req.id)}
|
|
className="bg-primary-blue text-white"
|
|
>
|
|
Approve
|
|
</Button>
|
|
<Button
|
|
data-testid={`join-request-${req.id}-reject`}
|
|
onClick={() => onReject(req.id)}
|
|
className="bg-iron-gray text-gray-200"
|
|
>
|
|
Reject
|
|
</Button>
|
|
</div>
|
|
</div>
|
|
))}
|
|
</div>
|
|
) : (
|
|
<Text size="sm" className="text-gray-500">
|
|
No pending join requests.
|
|
</Text>
|
|
)}
|
|
</Section>
|
|
|
|
<Section>
|
|
<Text size="lg" weight="semibold" className="text-white">
|
|
Members
|
|
</Text>
|
|
|
|
{loading ? (
|
|
<Text size="sm" className="text-gray-400">
|
|
Loading…
|
|
</Text>
|
|
) : members.length ? (
|
|
<div className="space-y-2">
|
|
{members.map((member) => (
|
|
<div
|
|
key={member.driverId}
|
|
className="flex flex-col md:flex-row md:items-center md:justify-between gap-3 bg-deep-graphite border border-charcoal-outline rounded p-3"
|
|
>
|
|
<div className="min-w-0">
|
|
<Text weight="medium" className="text-white truncate">
|
|
{(member.driver as any)?.name || 'Unknown'}
|
|
</Text>
|
|
<Text size="xs" className="text-gray-400 truncate">
|
|
{member.joinedAt}
|
|
</Text>
|
|
</div>
|
|
|
|
<div className="flex flex-col md:flex-row md:items-center gap-2">
|
|
<label className="text-xs text-gray-400" htmlFor={`role-${member.driverId}`}>
|
|
Role for {(member.driver as any)?.name || 'Unknown'}
|
|
</label>
|
|
<Select
|
|
id={`role-${member.driverId}`}
|
|
aria-label={`Role for ${(member.driver as any)?.name || 'Unknown'}`}
|
|
value={member.role}
|
|
onChange={(e) => onRoleChange(member.driverId, e.target.value as MembershipRole)}
|
|
options={roleOptions.map((role) => ({ value: role, label: role }))}
|
|
className="bg-iron-gray text-white px-3 py-2 rounded"
|
|
/>
|
|
<Button
|
|
data-testid={`member-${member.driverId}-remove`}
|
|
onClick={() => onRemove(member.driverId)}
|
|
className="bg-iron-gray text-gray-200"
|
|
>
|
|
Remove
|
|
</Button>
|
|
</div>
|
|
</div>
|
|
))}
|
|
</div>
|
|
) : (
|
|
<Text size="sm" className="text-gray-500">
|
|
No members found.
|
|
</Text>
|
|
)}
|
|
</Section>
|
|
</Section>
|
|
</Card>
|
|
</Section>
|
|
);
|
|
} |