'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, } 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'; 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' | 'disputes'>('members'); const [rejectReason, setRejectReason] = useState(''); const [configForm, setConfigForm] = useState(null); const [configLoading, setConfigLoading] = 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]); 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(`/leagues/${league.id}/races/${race.id}`); }} />
)} {activeTab === 'disputes' && (

Disputes (Alpha)

Demo-only view of potential protest and dispute workflow for this league.

Sample Protest

Driver contact in Turn 3, Lap 12. Protest submitted by a driver against another competitor for avoidable contact.

In the full product, this area would show protests, steward reviews, penalties, and appeals.

For the alpha, this tab is static and read-only and does not affect any race or league state.

)} {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.