'use client'; import { useState, useEffect, useCallback, useMemo } from 'react'; import { useRouter, useSearchParams, usePathname } from 'next/navigation'; import Button from '../ui/Button'; import Card from '../ui/Card'; import LeagueMembers from './LeagueMembers'; import ScheduleRaceForm from './ScheduleRaceForm'; import { League } from '@gridpilot/racing/domain/entities/League'; import { getLeagueMembershipRepository, getDriverStats, getAllDriverRankings, getDriverRepository, getGetLeagueFullConfigQuery, getRaceRepository, getProtestRepository, } from '@/lib/di-container'; import type { LeagueConfigFormModel } from '@gridpilot/racing/application'; import { LeagueBasicsSection } from './LeagueBasicsSection'; import { LeagueStructureSection } from './LeagueStructureSection'; import { LeagueScoringSection } from './LeagueScoringSection'; import { LeagueDropSection } from './LeagueDropSection'; import { LeagueTimingsSection } from './LeagueTimingsSection'; import { useEffectiveDriverId } from '@/lib/currentDriver'; import type { MembershipRole } from '@/lib/leagueMembership'; import type { DriverDTO } from '@gridpilot/racing/application/dto/DriverDTO'; import { EntityMappers } from '@gridpilot/racing/application/mappers/EntityMappers'; import DriverSummaryPill from '@/components/profile/DriverSummaryPill'; import DriverIdentity from '@/components/drivers/DriverIdentity'; import Modal from '@/components/ui/Modal'; import { AlertTriangle, CheckCircle, Clock, XCircle, Flag, Calendar, User } from 'lucide-react'; import type { Protest } from '@gridpilot/racing/domain/entities/Protest'; import type { Race } from '@gridpilot/racing/domain/entities/Race'; interface JoinRequest { id: string; leagueId: string; driverId: string; requestedAt: Date; message?: string; } interface LeagueAdminProps { league: League; onLeagueUpdate?: () => void; } export default function LeagueAdmin({ league, onLeagueUpdate }: LeagueAdminProps) { const router = useRouter(); const searchParams = useSearchParams(); const pathname = usePathname(); const currentDriverId = useEffectiveDriverId(); const [joinRequests, setJoinRequests] = useState([]); const [requestDriversById, setRequestDriversById] = useState>({}); const [ownerDriver, setOwnerDriver] = useState(null); const [loading, setLoading] = useState(true); const [error, setError] = useState(null); const [activeTab, setActiveTab] = useState<'members' | 'requests' | 'races' | 'settings' | 'protests'>('members'); const [rejectReason, setRejectReason] = useState(''); const [configForm, setConfigForm] = useState(null); const [configLoading, setConfigLoading] = useState(false); const [protests, setProtests] = useState([]); const [protestRaces, setProtestRaces] = useState>({}); const [protestDriversById, setProtestDriversById] = useState>({}); const [protestsLoading, setProtestsLoading] = useState(false); const loadJoinRequests = useCallback(async () => { setLoading(true); try { const membershipRepo = getLeagueMembershipRepository(); const requests = await membershipRepo.getJoinRequests(league.id); setJoinRequests(requests); const driverRepo = getDriverRepository(); const uniqueDriverIds = Array.from(new Set(requests.map((r) => r.driverId))); const driverEntities = await Promise.all( uniqueDriverIds.map((id) => driverRepo.findById(id)), ); const driverDtos = driverEntities .map((driver) => (driver ? EntityMappers.toDriverDTO(driver) : null)) .filter((dto): dto is DriverDTO => dto !== null); const byId: Record = {}; for (const dto of driverDtos) { byId[dto.id] = dto; } setRequestDriversById(byId); } catch (err) { console.error('Failed to load join requests:', err); } finally { setLoading(false); } }, [league.id]); useEffect(() => { loadJoinRequests(); }, [loadJoinRequests]); useEffect(() => { async function loadOwner() { try { const driverRepo = getDriverRepository(); const entity = await driverRepo.findById(league.ownerId); setOwnerDriver(EntityMappers.toDriverDTO(entity)); } catch (err) { console.error('Failed to load league owner:', err); } } loadOwner(); }, [league.ownerId]); useEffect(() => { async function loadConfig() { setConfigLoading(true); try { const query = getGetLeagueFullConfigQuery(); const form = await query.execute({ leagueId: league.id }); setConfigForm(form); } catch (err) { console.error('Failed to load league config:', err); } finally { setConfigLoading(false); } } loadConfig(); }, [league.id]); // Load protests for this league's races useEffect(() => { async function loadProtests() { setProtestsLoading(true); try { const raceRepo = getRaceRepository(); const protestRepo = getProtestRepository(); const driverRepo = getDriverRepository(); // Get all races for this league const leagueRaces = await raceRepo.findByLeagueId(league.id); // Get protests for each race const allProtests: Protest[] = []; const racesById: Record = {}; for (const race of leagueRaces) { racesById[race.id] = race; const raceProtests = await protestRepo.findByRaceId(race.id); allProtests.push(...raceProtests); } setProtests(allProtests); setProtestRaces(racesById); // Load driver info for all protesters and accused const driverIds = new Set(); allProtests.forEach((p) => { driverIds.add(p.protestingDriverId); driverIds.add(p.accusedDriverId); }); const driverEntities = await Promise.all( Array.from(driverIds).map((id) => driverRepo.findById(id)), ); const driverDtos = driverEntities .map((driver) => (driver ? EntityMappers.toDriverDTO(driver) : null)) .filter((dto): dto is DriverDTO => dto !== null); const byId: Record = {}; for (const dto of driverDtos) { byId[dto.id] = dto; } setProtestDriversById(byId); } catch (err) { console.error('Failed to load protests:', err); } finally { setProtestsLoading(false); } } if (activeTab === 'protests') { loadProtests(); } }, [league.id, activeTab]); const handleApproveRequest = async (requestId: string) => { try { const membershipRepo = getLeagueMembershipRepository(); const requests = await membershipRepo.getJoinRequests(league.id); const request = requests.find((r) => r.id === requestId); if (!request) { throw new Error('Join request not found'); } await membershipRepo.saveMembership({ leagueId: request.leagueId, driverId: request.driverId, role: 'member', status: 'active', joinedAt: new Date(), }); await membershipRepo.removeJoinRequest(requestId); await loadJoinRequests(); onLeagueUpdate?.(); } catch (err) { setError(err instanceof Error ? err.message : 'Failed to approve request'); } }; const handleRejectRequest = async (requestId: string, trimmedReason: string) => { try { const membershipRepo = getLeagueMembershipRepository(); // Alpha-only: we do not persist the reason yet, but we at least log it. console.log('Join request rejected with reason:', { requestId, reason: trimmedReason, }); await membershipRepo.removeJoinRequest(requestId); await loadJoinRequests(); } catch (err) { setError(err instanceof Error ? err.message : 'Failed to reject request'); } }; const handleRemoveMember = async (driverId: string) => { if (!confirm('Are you sure you want to remove this member?')) { return; } try { const membershipRepo = getLeagueMembershipRepository(); const performer = await membershipRepo.getMembership(league.id, currentDriverId); if (!performer || (performer.role !== 'owner' && performer.role !== 'admin')) { throw new Error('Only owners or admins can remove members'); } const membership = await membershipRepo.getMembership(league.id, driverId); if (!membership) { throw new Error('Member not found'); } if (membership.role === 'owner') { throw new Error('Cannot remove the league owner'); } await membershipRepo.removeMembership(league.id, driverId); onLeagueUpdate?.(); } catch (err) { setError(err instanceof Error ? err.message : 'Failed to remove member'); } }; const handleUpdateRole = async (driverId: string, newRole: MembershipRole) => { try { const membershipRepo = getLeagueMembershipRepository(); const performer = await membershipRepo.getMembership(league.id, currentDriverId); if (!performer || performer.role !== 'owner') { throw new Error('Only the league owner can update roles'); } const membership = await membershipRepo.getMembership(league.id, 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, }); onLeagueUpdate?.(); } catch (err) { setError(err instanceof Error ? err.message : 'Failed to update role'); } }; const modal = searchParams?.get('modal'); const modalRequestId = searchParams?.get('requestId'); const activeRejectRequest = modal === 'reject-request' ? joinRequests.find((r) => r.id === modalRequestId) ?? null : null; useEffect(() => { if (!activeRejectRequest) { setRejectReason(''); } }, [activeRejectRequest, setRejectReason]); const isRejectModalOpen = modal === 'reject-request' && !!activeRejectRequest; const openRejectModal = (requestId: string) => { const params = new URLSearchParams(searchParams?.toString()); params.set('modal', 'reject-request'); params.set('requestId', requestId); const query = params.toString(); const url = query ? `${pathname}?${query}` : pathname; router.push(url, { scroll: false }); }; const closeModal = () => { const params = new URLSearchParams(searchParams?.toString()); params.delete('modal'); params.delete('requestId'); const query = params.toString(); const url = query ? `${pathname}?${query}` : pathname; router.push(url, { scroll: false }); }; const ownerSummary = useMemo(() => { if (!ownerDriver) { return null; } const stats = getDriverStats(ownerDriver.id); const allRankings = getAllDriverRankings(); let rating: number | null = stats?.rating ?? null; let rank: number | null = null; if (stats) { if (typeof stats.overallRank === 'number' && stats.overallRank > 0) { rank = stats.overallRank; } else { const indexInGlobal = allRankings.findIndex( (stat) => stat.driverId === stats.driverId, ); if (indexInGlobal !== -1) { rank = indexInGlobal + 1; } } if (rating === null) { const globalEntry = allRankings.find( (stat) => stat.driverId === stats.driverId, ); if (globalEntry) { rating = globalEntry.rating; } } } return { driver: ownerDriver, rating, rank, }; }, [ownerDriver]); return (
{error && (
{error}
)} {/* Admin Tabs */}
{/* Tab Content */} {activeTab === 'members' && (

Manage Members

)} {activeTab === 'requests' && (

Join Requests

{loading ? (
Loading requests...
) : joinRequests.length === 0 ? (
No pending join requests
) : (
{joinRequests.map((request) => { const driver = requestDriversById[request.driverId]; const requestedOn = new Date(request.requestedAt).toLocaleDateString('en-US', { month: 'short', day: 'numeric', year: 'numeric', }); const metaPieces = [ `Requested ${requestedOn}`, request.message ? `Message: "${request.message}"` : null, ].filter(Boolean); const meta = metaPieces.join(' • '); return (
{driver ? ( ) : (

Unknown Driver

Unable to load driver details

)}
); })}
)}
)} {activeTab === 'races' && (

Schedule Race

Create a new race for this league; this is an alpha-only in-memory scheduler.

{ router.push(`/races/${race.id}`); }} />
)} {activeTab === 'protests' && (

Protests

Review protests filed by drivers and manage steward decisions

Alpha Preview
{protestsLoading ? (
Loading protests...
) : protests.length === 0 ? (

No Protests Filed

When drivers file protests for incidents during races, they will appear here for steward review.

) : (
{/* Stats summary */}
Pending
{protests.filter((p) => p.status === 'pending').length}
Resolved
{protests.filter((p) => p.status === 'upheld' || p.status === 'dismissed').length}
Total
{protests.length}
{/* Protest list */}
{protests.map((protest) => { const race = protestRaces[protest.raceId]; const filer = protestDriversById[protest.protestingDriverId]; const accused = protestDriversById[protest.accusedDriverId]; const statusConfig = { pending: { color: 'text-warning-amber', bg: 'bg-warning-amber/10', border: 'border-warning-amber/30', icon: Clock, label: 'Pending Review' }, under_review: { color: 'text-primary-blue', bg: 'bg-primary-blue/10', border: 'border-primary-blue/30', icon: Flag, label: 'Under Review' }, awaiting_defense: { color: 'text-purple-400', bg: 'bg-purple-500/10', border: 'border-purple-500/30', icon: Clock, label: 'Awaiting Defense' }, upheld: { color: 'text-red-400', bg: 'bg-red-500/10', border: 'border-red-500/30', icon: AlertTriangle, label: 'Upheld' }, dismissed: { color: 'text-gray-400', bg: 'bg-gray-500/10', border: 'border-gray-500/30', icon: XCircle, label: 'Dismissed' }, withdrawn: { color: 'text-gray-500', bg: 'bg-gray-600/10', border: 'border-gray-600/30', icon: XCircle, label: 'Withdrawn' }, }[protest.status] ?? { color: 'text-gray-400', bg: 'bg-gray-500/10', border: 'border-gray-500/30', icon: Clock, label: protest.status }; const StatusIcon = statusConfig.icon; return (
{statusConfig.label}
{race && ( {race.track} • {new Date(race.scheduledAt).toLocaleDateString()} )}

Incident at Lap {protest.incident.lap}

{protest.incident.description}

Filed by: {filer?.name ?? 'Unknown'}
Against: {accused?.name ?? 'Unknown'}
{protest.status === 'pending' && (
)}
{protest.comment && (
Driver comment: "{protest.comment}"
)}
); })}

Alpha Note: Protest review and penalty application is demonstration-only. In the full product, stewards can review evidence, apply penalties, and manage appeals.

)}
)} {activeTab === 'settings' && (

League Settings

{configLoading && !configForm ? (
Loading configuration…
) : configForm ? (

Alpha Demo Season

At a glance

Structure:{' '} {configForm.structure.mode === 'solo' ? `Solo • ${configForm.structure.maxDrivers} drivers` : `Teams • ${configForm.structure.maxTeams ?? '—'} × ${ configForm.structure.driversPerTeam ?? '—' } drivers (${configForm.structure.maxDrivers ?? '—'} total)`}

Schedule:{' '} {`${configForm.timings.roundsPlanned ?? '?'} rounds • ${ configForm.timings.qualifyingMinutes } min Qualifying • ${configForm.timings.mainRaceMinutes} min Race`}

Scoring:{' '} {league.settings.pointsSystem.toUpperCase()}

League Owner

{ownerSummary ? ( ) : (

Loading owner details...

)}

League settings editing is alpha-only and changes are not persisted yet.

) : (
Unable to load league configuration for this demo league.
)}
)} { const trimmed = rejectReason.trim(); if (!trimmed) { setError('A rejection reason is required to reject a join request.'); return; } if (!activeRejectRequest) { return; } await handleRejectRequest(activeRejectRequest.id, trimmed); closeModal(); }} onSecondaryAction={() => { setRejectReason(''); }} onOpenChange={(open) => { if (!open) { closeModal(); } }} isOpen={isRejectModalOpen} >

This will remove the join request and the driver will not be added to the league.