307 lines
11 KiB
TypeScript
307 lines
11 KiB
TypeScript
'use client';
|
|
|
|
import { useState, useEffect, useMemo } from 'react';
|
|
import { useParams, useRouter } from 'next/navigation';
|
|
import Card from '@/components/ui/Card';
|
|
import Button from '@/components/ui/Button';
|
|
import {
|
|
getLeagueRepository,
|
|
getDriverRepository,
|
|
getGetLeagueFullConfigUseCase,
|
|
getLeagueMembershipRepository,
|
|
getDriverStats,
|
|
getAllDriverRankings,
|
|
getListLeagueScoringPresetsUseCase,
|
|
getTransferLeagueOwnershipUseCase
|
|
} from '@/lib/di-container';
|
|
import { LeagueFullConfigPresenter } from '@/lib/presenters/LeagueFullConfigPresenter';
|
|
import { LeagueScoringPresetsPresenter } from '@/lib/presenters/LeagueScoringPresetsPresenter';
|
|
import { useEffectiveDriverId } from '@/lib/currentDriver';
|
|
import { isLeagueAdminOrHigherRole } from '@/lib/leagueRoles';
|
|
import { ScoringPatternSection, ChampionshipsSection } from '@/components/leagues/LeagueScoringSection';
|
|
import { LeagueDropSection } from '@/components/leagues/LeagueDropSection';
|
|
import { ReadonlyLeagueInfo } from '@/components/leagues/ReadonlyLeagueInfo';
|
|
import type { LeagueConfigFormModel } from '@gridpilot/racing/application';
|
|
import type { League } from '@gridpilot/racing/domain/entities/League';
|
|
import type { DriverDTO } from '@gridpilot/racing/application/dto/DriverDTO';
|
|
import type { LeagueScoringPresetDTO } from '@gridpilot/racing/application/ports/LeagueScoringPresetProvider';
|
|
import { EntityMappers } from '@gridpilot/racing/application/mappers/EntityMappers';
|
|
import DriverSummaryPill from '@/components/profile/DriverSummaryPill';
|
|
import { AlertTriangle, Settings, Trophy, Calendar, TrendingDown, Edit, Users, UserCog } from 'lucide-react';
|
|
|
|
export default function LeagueSettingsPage() {
|
|
const params = useParams();
|
|
const leagueId = params.id as string;
|
|
const currentDriverId = useEffectiveDriverId();
|
|
|
|
const [league, setLeague] = useState<League | null>(null);
|
|
const [configForm, setConfigForm] = useState<LeagueConfigFormModel | null>(null);
|
|
const [presets, setPresets] = useState<LeagueScoringPresetDTO[]>([]);
|
|
const [ownerDriver, setOwnerDriver] = useState<DriverDTO | null>(null);
|
|
const [loading, setLoading] = useState(true);
|
|
const [isAdmin, setIsAdmin] = useState(false);
|
|
const [showTransferDialog, setShowTransferDialog] = useState(false);
|
|
const [selectedNewOwner, setSelectedNewOwner] = useState<string>('');
|
|
const [transferring, setTransferring] = useState(false);
|
|
const [allMembers, setAllMembers] = useState<DriverDTO[]>([]);
|
|
const router = useRouter();
|
|
|
|
useEffect(() => {
|
|
async function checkAdmin() {
|
|
const membershipRepo = getLeagueMembershipRepository();
|
|
const membership = await membershipRepo.getMembership(leagueId, currentDriverId);
|
|
setIsAdmin(membership ? isLeagueAdminOrHigherRole(membership.role) : false);
|
|
}
|
|
checkAdmin();
|
|
}, [leagueId, currentDriverId]);
|
|
|
|
useEffect(() => {
|
|
async function loadSettings() {
|
|
setLoading(true);
|
|
try {
|
|
const leagueRepo = getLeagueRepository();
|
|
const driverRepo = getDriverRepository();
|
|
const useCase = getGetLeagueFullConfigUseCase();
|
|
const presetsUseCase = getListLeagueScoringPresetsUseCase();
|
|
|
|
const leagueData = await leagueRepo.findById(leagueId);
|
|
if (!leagueData) {
|
|
setLoading(false);
|
|
return;
|
|
}
|
|
|
|
setLeague(leagueData);
|
|
|
|
const configPresenter = new LeagueFullConfigPresenter();
|
|
await useCase.execute({ leagueId }, configPresenter);
|
|
const configViewModel = configPresenter.getViewModel();
|
|
if (configViewModel) {
|
|
setConfigForm(configViewModel as LeagueConfigFormModel);
|
|
}
|
|
|
|
const presetsPresenter = new LeagueScoringPresetsPresenter();
|
|
await presetsUseCase.execute(undefined as void, presetsPresenter);
|
|
const presetsViewModel = presetsPresenter.getViewModel();
|
|
setPresets(presetsViewModel.presets);
|
|
|
|
const entity = await driverRepo.findById(leagueData.ownerId);
|
|
if (entity) {
|
|
setOwnerDriver(EntityMappers.toDriverDTO(entity));
|
|
}
|
|
|
|
const membershipRepo = getLeagueMembershipRepository();
|
|
const memberships = await membershipRepo.getLeagueMembers(leagueId);
|
|
const memberDrivers: DriverDTO[] = [];
|
|
for (const m of memberships) {
|
|
if (m.driverId !== leagueData.ownerId && m.status === 'active') {
|
|
const d = await driverRepo.findById(m.driverId);
|
|
if (d) {
|
|
const dto = EntityMappers.toDriverDTO(d);
|
|
if (dto) {
|
|
memberDrivers.push(dto);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
setAllMembers(memberDrivers);
|
|
} catch (err) {
|
|
console.error('Failed to load league settings:', err);
|
|
} finally {
|
|
setLoading(false);
|
|
}
|
|
}
|
|
|
|
if (isAdmin) {
|
|
loadSettings();
|
|
}
|
|
}, [leagueId, isAdmin]);
|
|
|
|
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]);
|
|
|
|
const handleTransferOwnership = async () => {
|
|
if (!selectedNewOwner || !league) return;
|
|
|
|
setTransferring(true);
|
|
try {
|
|
const useCase = getTransferLeagueOwnershipUseCase();
|
|
await useCase.execute({
|
|
leagueId,
|
|
currentOwnerId: currentDriverId,
|
|
newOwnerId: selectedNewOwner,
|
|
});
|
|
|
|
setShowTransferDialog(false);
|
|
router.refresh();
|
|
} catch (err) {
|
|
console.error('Failed to transfer ownership:', err);
|
|
alert(err instanceof Error ? err.message : 'Failed to transfer ownership');
|
|
} finally {
|
|
setTransferring(false);
|
|
}
|
|
};
|
|
|
|
if (!isAdmin) {
|
|
return (
|
|
<Card>
|
|
<div className="text-center py-12">
|
|
<div className="w-16 h-16 mx-auto mb-4 rounded-full bg-iron-gray/50 flex items-center justify-center">
|
|
<AlertTriangle className="w-8 h-8 text-warning-amber" />
|
|
</div>
|
|
<h3 className="text-lg font-medium text-white mb-2">Admin Access Required</h3>
|
|
<p className="text-sm text-gray-400">
|
|
Only league admins can access settings.
|
|
</p>
|
|
</div>
|
|
</Card>
|
|
);
|
|
}
|
|
|
|
if (loading) {
|
|
return (
|
|
<Card>
|
|
<div className="py-6 text-sm text-gray-400">Loading configuration…</div>
|
|
</Card>
|
|
);
|
|
}
|
|
|
|
if (!configForm || !league) {
|
|
return (
|
|
<Card>
|
|
<div className="py-6 text-sm text-gray-500">
|
|
Unable to load league configuration for this demo league.
|
|
</div>
|
|
</Card>
|
|
);
|
|
}
|
|
|
|
return (
|
|
<div className="space-y-6">
|
|
{/* Header */}
|
|
<div className="flex items-center gap-3">
|
|
<div className="flex h-12 w-12 items-center justify-center rounded-xl bg-primary-blue/10">
|
|
<Settings className="w-6 h-6 text-primary-blue" />
|
|
</div>
|
|
<div>
|
|
<h1 className="text-2xl font-bold text-white">League Settings</h1>
|
|
<p className="text-sm text-gray-400">Manage your league configuration</p>
|
|
</div>
|
|
</div>
|
|
|
|
|
|
|
|
{/* READONLY INFORMATION SECTION - Compact */}
|
|
<div className="space-y-4">
|
|
<ReadonlyLeagueInfo league={league} configForm={configForm} />
|
|
|
|
{/* League Owner - Compact */}
|
|
<div className="rounded-xl border border-charcoal-outline bg-gradient-to-br from-iron-gray/40 to-iron-gray/20 p-5">
|
|
<h3 className="text-sm font-semibold text-gray-400 mb-3">League Owner</h3>
|
|
{ownerSummary ? (
|
|
<DriverSummaryPill
|
|
driver={ownerSummary.driver}
|
|
rating={ownerSummary.rating}
|
|
rank={ownerSummary.rank}
|
|
/>
|
|
) : (
|
|
<p className="text-sm text-gray-500">Loading owner details...</p>
|
|
)}
|
|
</div>
|
|
|
|
{/* Transfer Ownership - Owner Only */}
|
|
{league.ownerId === currentDriverId && allMembers.length > 0 && (
|
|
<div className="rounded-xl border border-charcoal-outline bg-gradient-to-br from-iron-gray/40 to-iron-gray/20 p-5">
|
|
<div className="flex items-center gap-2 mb-3">
|
|
<UserCog className="w-4 h-4 text-gray-400" />
|
|
<h3 className="text-sm font-semibold text-gray-400">Transfer Ownership</h3>
|
|
</div>
|
|
<p className="text-xs text-gray-500 mb-4">
|
|
Transfer league ownership to another active member. You will become an admin.
|
|
</p>
|
|
|
|
{!showTransferDialog ? (
|
|
<Button
|
|
variant="secondary"
|
|
onClick={() => setShowTransferDialog(true)}
|
|
>
|
|
Transfer Ownership
|
|
</Button>
|
|
) : (
|
|
<div className="space-y-3">
|
|
<select
|
|
value={selectedNewOwner}
|
|
onChange={(e) => setSelectedNewOwner(e.target.value)}
|
|
className="w-full rounded-lg border border-charcoal-outline bg-charcoal-card px-3 py-2 text-sm text-white focus:border-primary-blue focus:outline-none"
|
|
>
|
|
<option value="">Select new owner...</option>
|
|
{allMembers.map((member) => (
|
|
<option key={member.id} value={member.id}>
|
|
{member.name}
|
|
</option>
|
|
))}
|
|
</select>
|
|
|
|
<div className="flex gap-2">
|
|
<Button
|
|
variant="primary"
|
|
onClick={handleTransferOwnership}
|
|
disabled={!selectedNewOwner || transferring}
|
|
>
|
|
{transferring ? 'Transferring...' : 'Confirm Transfer'}
|
|
</Button>
|
|
<Button
|
|
variant="secondary"
|
|
onClick={() => {
|
|
setShowTransferDialog(false);
|
|
setSelectedNewOwner('');
|
|
}}
|
|
disabled={transferring}
|
|
>
|
|
Cancel
|
|
</Button>
|
|
</div>
|
|
</div>
|
|
)}
|
|
</div>
|
|
)}
|
|
</div>
|
|
</div>
|
|
);
|
|
} |