This commit is contained in:
2025-12-12 14:23:40 +01:00
parent 6a88fe93ab
commit 2cd3bfbb47
58 changed files with 2866 additions and 260 deletions

View File

@@ -15,9 +15,11 @@ import {
loadLeagueProtests,
removeLeagueMember as removeLeagueMemberCommand,
updateLeagueMemberRole as updateLeagueMemberRoleCommand,
loadLeagueSeasons,
type LeagueJoinRequestViewModel,
type LeagueOwnerSummaryViewModel,
type LeagueAdminProtestsViewModel,
type LeagueSeasonSummaryViewModel,
} from '@/lib/presenters/LeagueAdminPresenter';
import type { LeagueConfigFormModel } from '@gridpilot/racing/application';
import type { LeagueSummaryViewModel } from '@/lib/presenters/LeagueAdminPresenter';
@@ -51,13 +53,15 @@ export default function LeagueAdmin({ league, onLeagueUpdate }: LeagueAdminProps
const [ownerSummary, setOwnerSummary] = useState<LeagueOwnerSummaryViewModel | null>(null);
const [loading, setLoading] = useState(true);
const [error, setError] = useState<string | null>(null);
const [activeTab, setActiveTab] = useState<'members' | 'requests' | 'races' | 'settings' | 'protests' | 'sponsorships' | 'fees' | 'wallet' | 'prizes' | 'liveries'>('members');
const [activeTab, setActiveTab] = useState<'members' | 'requests' | 'races' | 'seasons' | 'settings' | 'protests' | 'sponsorships' | 'fees' | 'wallet' | 'prizes' | 'liveries'>('members');
const [downloadingLiveryPack, setDownloadingLiveryPack] = useState(false);
const [rejectReason, setRejectReason] = useState('');
const [configForm, setConfigForm] = useState<LeagueConfigFormModel | null>(null);
const [configLoading, setConfigLoading] = useState(false);
const [protestsViewModel, setProtestsViewModel] = useState<LeagueAdminProtestsViewModel | null>(null);
const [protestsLoading, setProtestsLoading] = useState(false);
const [seasons, setSeasons] = useState<LeagueSeasonSummaryViewModel[]>([]);
const [seasonsLoading, setSeasonsLoading] = useState(false);
const loadJoinRequests = useCallback(async () => {
setLoading(true);
@@ -104,6 +108,22 @@ export default function LeagueAdmin({ league, onLeagueUpdate }: LeagueAdminProps
loadConfig();
}, [league.id]);
useEffect(() => {
async function loadSeasonsVm() {
setSeasonsLoading(true);
try {
const items = await loadLeagueSeasons(league.id);
setSeasons(items);
} catch (err) {
console.error('Failed to load seasons:', err);
} finally {
setSeasonsLoading(false);
}
}
loadSeasonsVm();
}, [league.id]);
// Load protests for this league's races
useEffect(() => {
async function loadProtests() {
@@ -257,6 +277,16 @@ export default function LeagueAdmin({ league, onLeagueUpdate }: LeagueAdminProps
>
Races
</button>
<button
onClick={() => setActiveTab('seasons')}
className={`pb-3 px-1 font-medium transition-colors whitespace-nowrap ${
activeTab === 'seasons'
? 'text-primary-blue border-b-2 border-primary-blue'
: 'text-gray-400 hover:text-white'
}`}
>
Seasons
</button>
<button
onClick={() => setActiveTab('sponsorships')}
className={`pb-3 px-1 font-medium transition-colors whitespace-nowrap ${
@@ -429,6 +459,101 @@ export default function LeagueAdmin({ league, onLeagueUpdate }: LeagueAdminProps
</Card>
)}
{activeTab === 'seasons' && (
<Card>
<div className="flex items-center justify-between mb-4">
<div>
<h2 className="text-xl font-semibold text-white">Seasons</h2>
<p className="text-sm text-gray-400 mt-1">
Plan, run, and review seasons inside this league.
</p>
</div>
</div>
{seasonsLoading ? (
<div className="text-center py-8 text-gray-400">Loading seasons</div>
) : seasons.length === 0 ? (
<div className="text-center py-8">
<div className="w-12 h-12 mx-auto mb-3 rounded-full bg-iron-gray/50 flex items-center justify-center">
<Calendar className="w-6 h-6 text-gray-500" />
</div>
<p className="text-gray-400 text-sm">No seasons yet.</p>
<p className="text-gray-500 text-xs mt-1">
Your first season is created when you set up the league.
</p>
</div>
) : (
<div className="space-y-3">
{(() => {
const activeCount = seasons.filter((s) => s.status === 'active').length;
return seasons.map((season) => {
const start = season.startDate ? new Date(season.startDate) : null;
const end = season.endDate ? new Date(season.endDate) : null;
const hasParallel = activeCount > 1 && season.status === 'active';
const statusConfig: { label: string; className: string } = (() => {
switch (season.status) {
case 'planned':
return { label: 'Planned', className: 'bg-charcoal-outline/40 text-gray-300 border-charcoal-outline/60' };
case 'active':
return { label: 'Active', className: 'bg-performance-green/15 text-performance-green border-performance-green/40' };
case 'completed':
return { label: 'Completed', className: 'bg-primary-blue/15 text-primary-blue border-primary-blue/40' };
case 'archived':
return { label: 'Archived', className: 'bg-gray-600/20 text-gray-300 border-gray-600/40' };
case 'cancelled':
case 'canceled':
return { label: 'Cancelled', className: 'bg-red-500/10 text-red-400 border-red-500/40' };
default:
return { label: season.status, className: 'bg-charcoal-outline/40 text-gray-300 border-charcoal-outline/60' };
}
})();
return (
<div
key={season.seasonId}
className="rounded-lg border border-charcoal-outline bg-deep-graphite/70 p-4 flex items-start justify-between gap-4"
>
<div className="flex-1 min-w-0">
<div className="flex items-center gap-2 mb-1">
<h3 className="text-sm font-semibold text-white truncate">
{season.name}
</h3>
{season.isPrimary && (
<span className="px-2 py-0.5 rounded-full text-[10px] font-medium bg-primary-blue/10 text-primary-blue border border-primary-blue/40">
Primary
</span>
)}
{hasParallel && (
<span className="px-2 py-0.5 rounded-full text-[10px] font-medium bg-warning-amber/10 text-warning-amber border border-warning-amber/40">
Parallel
</span>
)}
</div>
<p className="text-xs text-gray-500">
{start
? end
? `${start.toLocaleDateString()} ${end.toLocaleDateString()}`
: `Starts ${start.toLocaleDateString()}`
: 'No dates configured yet'}
</p>
</div>
<div className="flex flex-col items-end gap-1">
<span
className={`px-2 py-0.5 rounded-full text-[10px] font-medium border ${statusConfig.className}`}
>
{statusConfig.label}
</span>
</div>
</div>
);
});
})()}
</div>
)}
</Card>
)}
{activeTab === 'protests' && (
<Card>
<div className="flex items-center justify-between mb-6">