di usage in website
This commit is contained in:
@@ -6,9 +6,9 @@ import { ReviewProtestModal } from '@/components/leagues/ReviewProtestModal';
|
||||
import StewardingStats from '@/components/leagues/StewardingStats';
|
||||
import Button from '@/components/ui/Button';
|
||||
import Card from '@/components/ui/Card';
|
||||
import { useEffectiveDriverId } from '@/hooks/useEffectiveDriverId';
|
||||
import { useServices } from '@/lib/services/ServiceProvider';
|
||||
import { LeagueStewardingViewModel } from '@/lib/view-models/LeagueStewardingViewModel';
|
||||
import { useCurrentDriver } from '@/hooks/driver/useCurrentDriver';
|
||||
import { useLeagueAdminStatus } from '@/hooks/league/useLeagueAdminStatus';
|
||||
import { useLeagueStewardingData } from '@/hooks/league/useLeagueStewardingData';
|
||||
import { LeagueRoleUtility } from '@/lib/utilities/LeagueRoleUtility';
|
||||
import {
|
||||
AlertCircle,
|
||||
@@ -25,15 +25,14 @@ import { useParams } from 'next/navigation';
|
||||
import { useMemo, useState } from 'react';
|
||||
|
||||
// Shared state components
|
||||
import { useDataFetching } from '@/components/shared/hooks/useDataFetching';
|
||||
import { StateContainer } from '@/components/shared/state/StateContainer';
|
||||
import { LoadingWrapper } from '@/components/shared/state/LoadingWrapper';
|
||||
|
||||
export default function LeagueStewardingPage() {
|
||||
const params = useParams();
|
||||
const leagueId = params.id as string;
|
||||
const currentDriverId = useEffectiveDriverId();
|
||||
const { leagueStewardingService, leagueMembershipService } = useServices();
|
||||
const { data: currentDriver } = useCurrentDriver();
|
||||
const currentDriverId = currentDriver?.id;
|
||||
|
||||
const [activeTab, setActiveTab] = useState<'pending' | 'history'>('pending');
|
||||
const [selectedProtest, setSelectedProtest] = useState<any | null>(null);
|
||||
@@ -41,28 +40,10 @@ export default function LeagueStewardingPage() {
|
||||
const [showQuickPenaltyModal, setShowQuickPenaltyModal] = useState(false);
|
||||
|
||||
// Check admin status
|
||||
const { data: isAdmin, isLoading: adminLoading } = useDataFetching({
|
||||
queryKey: ['leagueMembership', leagueId, currentDriverId],
|
||||
queryFn: async () => {
|
||||
const membership = await leagueMembershipService.getMembership(leagueId, currentDriverId);
|
||||
return membership ? LeagueRoleUtility.isLeagueAdminOrHigherRole(membership.role) : false;
|
||||
},
|
||||
});
|
||||
const { data: isAdmin, isLoading: adminLoading } = useLeagueAdminStatus(leagueId, currentDriverId || '');
|
||||
|
||||
// Load stewarding data (only if admin)
|
||||
const { data: stewardingData, isLoading: dataLoading, error, retry } = useDataFetching({
|
||||
queryKey: ['leagueStewarding', leagueId],
|
||||
queryFn: () => leagueStewardingService.getLeagueStewardingData(leagueId),
|
||||
enabled: !!isAdmin,
|
||||
onSuccess: (data) => {
|
||||
// Auto-expand races with pending protests
|
||||
const racesWithPending = new Set<string>();
|
||||
data.pendingRaces.forEach(race => {
|
||||
racesWithPending.add(race.race.id);
|
||||
});
|
||||
setExpandedRaces(racesWithPending);
|
||||
},
|
||||
});
|
||||
const { data: stewardingData, isLoading: dataLoading, error, retry } = useLeagueStewardingData(leagueId);
|
||||
|
||||
// Filter races based on active tab
|
||||
const filteredRaces = useMemo(() => {
|
||||
@@ -75,13 +56,6 @@ export default function LeagueStewardingPage() {
|
||||
penaltyValue: number,
|
||||
stewardNotes: string
|
||||
) => {
|
||||
await leagueStewardingService.reviewProtest({
|
||||
protestId,
|
||||
stewardId: currentDriverId,
|
||||
decision: 'uphold',
|
||||
decisionNotes: stewardNotes,
|
||||
});
|
||||
|
||||
// Find the protest to get details for penalty
|
||||
let foundProtest: any | undefined;
|
||||
stewardingData?.racesWithData.forEach(raceData => {
|
||||
@@ -91,16 +65,24 @@ export default function LeagueStewardingPage() {
|
||||
});
|
||||
|
||||
if (foundProtest) {
|
||||
await leagueStewardingService.applyPenalty({
|
||||
raceId: foundProtest.raceId,
|
||||
driverId: foundProtest.accusedDriverId,
|
||||
stewardId: currentDriverId,
|
||||
type: penaltyType,
|
||||
value: penaltyValue,
|
||||
reason: foundProtest.incident.description,
|
||||
protestId,
|
||||
notes: stewardNotes,
|
||||
});
|
||||
// TODO: Implement protest review and penalty application
|
||||
// await leagueStewardingService.reviewProtest({
|
||||
// protestId,
|
||||
// stewardId: currentDriverId,
|
||||
// decision: 'uphold',
|
||||
// decisionNotes: stewardNotes,
|
||||
// });
|
||||
|
||||
// await leagueStewardingService.applyPenalty({
|
||||
// raceId: foundProtest.raceId,
|
||||
// driverId: foundProtest.accusedDriverId,
|
||||
// stewardId: currentDriverId,
|
||||
// type: penaltyType,
|
||||
// value: penaltyValue,
|
||||
// reason: foundProtest.incident.description,
|
||||
// protestId,
|
||||
// notes: stewardNotes,
|
||||
// });
|
||||
}
|
||||
|
||||
// Retry to refresh data
|
||||
@@ -108,12 +90,13 @@ export default function LeagueStewardingPage() {
|
||||
};
|
||||
|
||||
const handleRejectProtest = async (protestId: string, stewardNotes: string) => {
|
||||
await leagueStewardingService.reviewProtest({
|
||||
protestId,
|
||||
stewardId: currentDriverId,
|
||||
decision: 'dismiss',
|
||||
decisionNotes: stewardNotes,
|
||||
});
|
||||
// TODO: Implement protest rejection
|
||||
// await leagueStewardingService.reviewProtest({
|
||||
// protestId,
|
||||
// stewardId: currentDriverId,
|
||||
// decision: 'dismiss',
|
||||
// decisionNotes: stewardNotes,
|
||||
// });
|
||||
|
||||
// Retry to refresh data
|
||||
await retry();
|
||||
@@ -185,245 +168,249 @@ export default function LeagueStewardingPage() {
|
||||
}
|
||||
}}
|
||||
>
|
||||
{(data) => (
|
||||
<div className="space-y-6">
|
||||
<Card>
|
||||
<div className="flex items-center justify-between mb-6">
|
||||
<div>
|
||||
<h2 className="text-xl font-semibold text-white">Stewarding</h2>
|
||||
<p className="text-sm text-gray-400 mt-1">
|
||||
Quick overview of protests and penalties across all races
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Stats summary */}
|
||||
<StewardingStats
|
||||
totalPending={data.totalPending}
|
||||
totalResolved={data.totalResolved}
|
||||
totalPenalties={data.totalPenalties}
|
||||
/>
|
||||
|
||||
{/* Tab navigation */}
|
||||
<div className="border-b border-charcoal-outline mb-6">
|
||||
<div className="flex gap-4">
|
||||
<button
|
||||
onClick={() => setActiveTab('pending')}
|
||||
className={`pb-3 px-1 font-medium transition-colors ${
|
||||
activeTab === 'pending'
|
||||
? 'text-primary-blue border-b-2 border-primary-blue'
|
||||
: 'text-gray-400 hover:text-white'
|
||||
}`}
|
||||
>
|
||||
Pending Protests
|
||||
{data.totalPending > 0 && (
|
||||
<span className="ml-2 px-2 py-0.5 text-xs bg-warning-amber/20 text-warning-amber rounded-full">
|
||||
{data.totalPending}
|
||||
</span>
|
||||
)}
|
||||
</button>
|
||||
<button
|
||||
onClick={() => setActiveTab('history')}
|
||||
className={`pb-3 px-1 font-medium transition-colors ${
|
||||
activeTab === 'history'
|
||||
? 'text-primary-blue border-b-2 border-primary-blue'
|
||||
: 'text-gray-400 hover:text-white'
|
||||
}`}
|
||||
>
|
||||
History
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Content */}
|
||||
{filteredRaces.length === 0 ? (
|
||||
<div className="text-center py-12">
|
||||
<div className="w-16 h-16 mx-auto mb-4 rounded-full bg-performance-green/10 flex items-center justify-center">
|
||||
<Flag className="w-8 h-8 text-performance-green" />
|
||||
{(data) => {
|
||||
if (!data) return null;
|
||||
|
||||
return (
|
||||
<div className="space-y-6">
|
||||
<Card>
|
||||
<div className="flex items-center justify-between mb-6">
|
||||
<div>
|
||||
<h2 className="text-xl font-semibold text-white">Stewarding</h2>
|
||||
<p className="text-sm text-gray-400 mt-1">
|
||||
Quick overview of protests and penalties across all races
|
||||
</p>
|
||||
</div>
|
||||
<p className="font-semibold text-lg text-white mb-2">
|
||||
{activeTab === 'pending' ? 'All Clear!' : 'No History Yet'}
|
||||
</p>
|
||||
<p className="text-sm text-gray-400">
|
||||
{activeTab === 'pending'
|
||||
? 'No pending protests to review'
|
||||
: 'No resolved protests or penalties'}
|
||||
</p>
|
||||
</div>
|
||||
) : (
|
||||
<div className="space-y-4">
|
||||
{filteredRaces.map(({ race, pendingProtests, resolvedProtests, penalties }) => {
|
||||
const isExpanded = expandedRaces.has(race.id);
|
||||
const displayProtests = activeTab === 'pending' ? pendingProtests : resolvedProtests;
|
||||
|
||||
return (
|
||||
<div key={race.id} className="rounded-lg border border-charcoal-outline overflow-hidden">
|
||||
{/* Race Header */}
|
||||
<button
|
||||
onClick={() => toggleRaceExpanded(race.id)}
|
||||
className="w-full px-4 py-3 bg-iron-gray/30 hover:bg-iron-gray/50 transition-colors flex items-center justify-between"
|
||||
>
|
||||
<div className="flex items-center gap-4">
|
||||
<div className="flex items-center gap-2">
|
||||
<MapPin className="w-4 h-4 text-gray-400" />
|
||||
<span className="font-medium text-white">{race.track}</span>
|
||||
</div>
|
||||
<div className="flex items-center gap-2 text-gray-400 text-sm">
|
||||
<Calendar className="w-4 h-4" />
|
||||
<span>{race.scheduledAt.toLocaleDateString()}</span>
|
||||
</div>
|
||||
{activeTab === 'pending' && pendingProtests.length > 0 && (
|
||||
<span className="px-2 py-0.5 text-xs font-medium bg-warning-amber/20 text-warning-amber rounded-full">
|
||||
{pendingProtests.length} pending
|
||||
</span>
|
||||
)}
|
||||
{activeTab === 'history' && (
|
||||
<span className="px-2 py-0.5 text-xs font-medium bg-gray-500/20 text-gray-400 rounded-full">
|
||||
{resolvedProtests.length} protests, {penalties.length} penalties
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
<ChevronRight className={`w-5 h-5 text-gray-400 transition-transform ${isExpanded ? 'rotate-90' : ''}`} />
|
||||
</button>
|
||||
|
||||
{/* Expanded Content */}
|
||||
{isExpanded && (
|
||||
<div className="p-4 space-y-3 bg-deep-graphite/50">
|
||||
{displayProtests.length === 0 && penalties.length === 0 ? (
|
||||
<p className="text-sm text-gray-400 text-center py-4">No items to display</p>
|
||||
) : (
|
||||
<>
|
||||
{displayProtests.map((protest) => {
|
||||
const protester = data.driverMap[protest.protestingDriverId];
|
||||
const accused = data.driverMap[protest.accusedDriverId];
|
||||
const daysSinceFiled = Math.floor((Date.now() - new Date(protest.filedAt).getTime()) / (1000 * 60 * 60 * 24));
|
||||
const isUrgent = daysSinceFiled > 2 && (protest.status === 'pending' || protest.status === 'under_review');
|
||||
|
||||
return (
|
||||
<div
|
||||
key={protest.id}
|
||||
className={`rounded-lg border border-charcoal-outline bg-iron-gray/30 p-4 ${isUrgent ? 'border-l-4 border-l-red-500' : ''}`}
|
||||
>
|
||||
<div className="flex items-start justify-between gap-4">
|
||||
<div className="flex-1 min-w-0">
|
||||
<div className="flex items-center gap-2 mb-2">
|
||||
<AlertCircle className="w-4 h-4 text-warning-amber flex-shrink-0" />
|
||||
<span className="font-medium text-white">
|
||||
{protester?.name || 'Unknown'} vs {accused?.name || 'Unknown'}
|
||||
</span>
|
||||
{getStatusBadge(protest.status)}
|
||||
{isUrgent && (
|
||||
<span className="px-2 py-0.5 text-xs font-medium bg-red-500/20 text-red-400 rounded-full flex items-center gap-1">
|
||||
<AlertTriangle className="w-3 h-3" />
|
||||
{daysSinceFiled}d old
|
||||
{/* Stats summary */}
|
||||
<StewardingStats
|
||||
totalPending={data.totalPending}
|
||||
totalResolved={data.totalResolved}
|
||||
totalPenalties={data.totalPenalties}
|
||||
/>
|
||||
|
||||
{/* Tab navigation */}
|
||||
<div className="border-b border-charcoal-outline mb-6">
|
||||
<div className="flex gap-4">
|
||||
<button
|
||||
onClick={() => setActiveTab('pending')}
|
||||
className={`pb-3 px-1 font-medium transition-colors ${
|
||||
activeTab === 'pending'
|
||||
? 'text-primary-blue border-b-2 border-primary-blue'
|
||||
: 'text-gray-400 hover:text-white'
|
||||
}`}
|
||||
>
|
||||
Pending Protests
|
||||
{data.totalPending > 0 && (
|
||||
<span className="ml-2 px-2 py-0.5 text-xs bg-warning-amber/20 text-warning-amber rounded-full">
|
||||
{data.totalPending}
|
||||
</span>
|
||||
)}
|
||||
</button>
|
||||
<button
|
||||
onClick={() => setActiveTab('history')}
|
||||
className={`pb-3 px-1 font-medium transition-colors ${
|
||||
activeTab === 'history'
|
||||
? 'text-primary-blue border-b-2 border-primary-blue'
|
||||
: 'text-gray-400 hover:text-white'
|
||||
}`}
|
||||
>
|
||||
History
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Content */}
|
||||
{filteredRaces.length === 0 ? (
|
||||
<div className="text-center py-12">
|
||||
<div className="w-16 h-16 mx-auto mb-4 rounded-full bg-performance-green/10 flex items-center justify-center">
|
||||
<Flag className="w-8 h-8 text-performance-green" />
|
||||
</div>
|
||||
<p className="font-semibold text-lg text-white mb-2">
|
||||
{activeTab === 'pending' ? 'All Clear!' : 'No History Yet'}
|
||||
</p>
|
||||
<p className="text-sm text-gray-400">
|
||||
{activeTab === 'pending'
|
||||
? 'No pending protests to review'
|
||||
: 'No resolved protests or penalties'}
|
||||
</p>
|
||||
</div>
|
||||
) : (
|
||||
<div className="space-y-4">
|
||||
{filteredRaces.map(({ race, pendingProtests, resolvedProtests, penalties }) => {
|
||||
const isExpanded = expandedRaces.has(race.id);
|
||||
const displayProtests = activeTab === 'pending' ? pendingProtests : resolvedProtests;
|
||||
|
||||
return (
|
||||
<div key={race.id} className="rounded-lg border border-charcoal-outline overflow-hidden">
|
||||
{/* Race Header */}
|
||||
<button
|
||||
onClick={() => toggleRaceExpanded(race.id)}
|
||||
className="w-full px-4 py-3 bg-iron-gray/30 hover:bg-iron-gray/50 transition-colors flex items-center justify-between"
|
||||
>
|
||||
<div className="flex items-center gap-4">
|
||||
<div className="flex items-center gap-2">
|
||||
<MapPin className="w-4 h-4 text-gray-400" />
|
||||
<span className="font-medium text-white">{race.track}</span>
|
||||
</div>
|
||||
<div className="flex items-center gap-2 text-gray-400 text-sm">
|
||||
<Calendar className="w-4 h-4" />
|
||||
<span>{race.scheduledAt.toLocaleDateString()}</span>
|
||||
</div>
|
||||
{activeTab === 'pending' && pendingProtests.length > 0 && (
|
||||
<span className="px-2 py-0.5 text-xs font-medium bg-warning-amber/20 text-warning-amber rounded-full">
|
||||
{pendingProtests.length} pending
|
||||
</span>
|
||||
)}
|
||||
{activeTab === 'history' && (
|
||||
<span className="px-2 py-0.5 text-xs font-medium bg-gray-500/20 text-gray-400 rounded-full">
|
||||
{resolvedProtests.length} protests, {penalties.length} penalties
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
<ChevronRight className={`w-5 h-5 text-gray-400 transition-transform ${isExpanded ? 'rotate-90' : ''}`} />
|
||||
</button>
|
||||
|
||||
{/* Expanded Content */}
|
||||
{isExpanded && (
|
||||
<div className="p-4 space-y-3 bg-deep-graphite/50">
|
||||
{displayProtests.length === 0 && penalties.length === 0 ? (
|
||||
<p className="text-sm text-gray-400 text-center py-4">No items to display</p>
|
||||
) : (
|
||||
<>
|
||||
{displayProtests.map((protest) => {
|
||||
const protester = data.driverMap[protest.protestingDriverId];
|
||||
const accused = data.driverMap[protest.accusedDriverId];
|
||||
const daysSinceFiled = Math.floor((Date.now() - new Date(protest.filedAt).getTime()) / (1000 * 60 * 60 * 24));
|
||||
const isUrgent = daysSinceFiled > 2 && (protest.status === 'pending' || protest.status === 'under_review');
|
||||
|
||||
return (
|
||||
<div
|
||||
key={protest.id}
|
||||
className={`rounded-lg border border-charcoal-outline bg-iron-gray/30 p-4 ${isUrgent ? 'border-l-4 border-l-red-500' : ''}`}
|
||||
>
|
||||
<div className="flex items-start justify-between gap-4">
|
||||
<div className="flex-1 min-w-0">
|
||||
<div className="flex items-center gap-2 mb-2">
|
||||
<AlertCircle className="w-4 h-4 text-warning-amber flex-shrink-0" />
|
||||
<span className="font-medium text-white">
|
||||
{protester?.name || 'Unknown'} vs {accused?.name || 'Unknown'}
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
<div className="flex items-center gap-4 text-sm text-gray-400 mb-2">
|
||||
<span>Lap {protest.incident.lap}</span>
|
||||
<span>•</span>
|
||||
<span>Filed {new Date(protest.filedAt).toLocaleDateString()}</span>
|
||||
{protest.proofVideoUrl && (
|
||||
<>
|
||||
<span>•</span>
|
||||
<span className="flex items-center gap-1 text-primary-blue">
|
||||
<Video className="w-3 h-3" />
|
||||
Video
|
||||
{getStatusBadge(protest.status)}
|
||||
{isUrgent && (
|
||||
<span className="px-2 py-0.5 text-xs font-medium bg-red-500/20 text-red-400 rounded-full flex items-center gap-1">
|
||||
<AlertTriangle className="w-3 h-3" />
|
||||
{daysSinceFiled}d old
|
||||
</span>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
<div className="flex items-center gap-4 text-sm text-gray-400 mb-2">
|
||||
<span>Lap {protest.incident.lap}</span>
|
||||
<span>•</span>
|
||||
<span>Filed {new Date(protest.filedAt).toLocaleDateString()}</span>
|
||||
{protest.proofVideoUrl && (
|
||||
<>
|
||||
<span>•</span>
|
||||
<span className="flex items-center gap-1 text-primary-blue">
|
||||
<Video className="w-3 h-3" />
|
||||
Video
|
||||
</span>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
<p className="text-sm text-gray-300 line-clamp-2">
|
||||
{protest.incident.description}
|
||||
</p>
|
||||
{protest.decisionNotes && (
|
||||
<div className="mt-2 p-2 rounded bg-iron-gray/50 border border-charcoal-outline/50">
|
||||
<p className="text-xs text-gray-400">
|
||||
<span className="font-medium">Steward:</span> {protest.decisionNotes}
|
||||
</p>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
<p className="text-sm text-gray-300 line-clamp-2">
|
||||
{protest.incident.description}
|
||||
</p>
|
||||
{protest.decisionNotes && (
|
||||
<div className="mt-2 p-2 rounded bg-iron-gray/50 border border-charcoal-outline/50">
|
||||
<p className="text-xs text-gray-400">
|
||||
<span className="font-medium">Steward:</span> {protest.decisionNotes}
|
||||
</p>
|
||||
</div>
|
||||
{(protest.status === 'pending' || protest.status === 'under_review') && (
|
||||
<Link href={`/leagues/${leagueId}/stewarding/protests/${protest.id}`}>
|
||||
<Button variant="primary">
|
||||
Review
|
||||
</Button>
|
||||
</Link>
|
||||
)}
|
||||
</div>
|
||||
{(protest.status === 'pending' || protest.status === 'under_review') && (
|
||||
<Link href={`/leagues/${leagueId}/stewarding/protests/${protest.id}`}>
|
||||
<Button variant="primary">
|
||||
Review
|
||||
</Button>
|
||||
</Link>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
);
|
||||
})}
|
||||
|
||||
{activeTab === 'history' && penalties.map((penalty) => {
|
||||
const driver = data.driverMap[penalty.driverId];
|
||||
return (
|
||||
<div
|
||||
key={penalty.id}
|
||||
className="rounded-lg border border-charcoal-outline bg-iron-gray/30 p-4"
|
||||
>
|
||||
<div className="flex items-center gap-3">
|
||||
<div className="w-8 h-8 rounded-full bg-red-500/20 flex items-center justify-center flex-shrink-0">
|
||||
<Gavel className="w-4 h-4 text-red-400" />
|
||||
</div>
|
||||
<div className="flex-1">
|
||||
<div className="flex items-center gap-2">
|
||||
<span className="font-medium text-white">{driver?.name || 'Unknown'}</span>
|
||||
<span className="px-2 py-0.5 text-xs font-medium bg-red-500/20 text-red-400 rounded-full">
|
||||
{penalty.type.replace('_', ' ')}
|
||||
{activeTab === 'history' && penalties.map((penalty) => {
|
||||
const driver = data.driverMap[penalty.driverId];
|
||||
return (
|
||||
<div
|
||||
key={penalty.id}
|
||||
className="rounded-lg border border-charcoal-outline bg-iron-gray/30 p-4"
|
||||
>
|
||||
<div className="flex items-center gap-3">
|
||||
<div className="w-8 h-8 rounded-full bg-red-500/20 flex items-center justify-center flex-shrink-0">
|
||||
<Gavel className="w-4 h-4 text-red-400" />
|
||||
</div>
|
||||
<div className="flex-1">
|
||||
<div className="flex items-center gap-2">
|
||||
<span className="font-medium text-white">{driver?.name || 'Unknown'}</span>
|
||||
<span className="px-2 py-0.5 text-xs font-medium bg-red-500/20 text-red-400 rounded-full">
|
||||
{penalty.type.replace('_', ' ')}
|
||||
</span>
|
||||
</div>
|
||||
<p className="text-sm text-gray-400">{penalty.reason}</p>
|
||||
</div>
|
||||
<div className="text-right">
|
||||
<span className="text-lg font-bold text-red-400">
|
||||
{penalty.type === 'time_penalty' && `+${penalty.value}s`}
|
||||
{penalty.type === 'grid_penalty' && `+${penalty.value} grid`}
|
||||
{penalty.type === 'points_deduction' && `-${penalty.value} pts`}
|
||||
{penalty.type === 'disqualification' && 'DSQ'}
|
||||
{penalty.type === 'warning' && 'Warning'}
|
||||
{penalty.type === 'license_points' && `${penalty.value} LP`}
|
||||
</span>
|
||||
</div>
|
||||
<p className="text-sm text-gray-400">{penalty.reason}</p>
|
||||
</div>
|
||||
<div className="text-right">
|
||||
<span className="text-lg font-bold text-red-400">
|
||||
{penalty.type === 'time_penalty' && `+${penalty.value}s`}
|
||||
{penalty.type === 'grid_penalty' && `+${penalty.value} grid`}
|
||||
{penalty.type === 'points_deduction' && `-${penalty.value} pts`}
|
||||
{penalty.type === 'disqualification' && 'DSQ'}
|
||||
{penalty.type === 'warning' && 'Warning'}
|
||||
{penalty.type === 'license_points' && `${penalty.value} LP`}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
)}
|
||||
</Card>
|
||||
|
||||
{activeTab === 'history' && (
|
||||
<PenaltyFAB onClick={() => setShowQuickPenaltyModal(true)} />
|
||||
)}
|
||||
</Card>
|
||||
|
||||
{activeTab === 'history' && (
|
||||
<PenaltyFAB onClick={() => setShowQuickPenaltyModal(true)} />
|
||||
)}
|
||||
{selectedProtest && (
|
||||
<ReviewProtestModal
|
||||
protest={selectedProtest}
|
||||
onClose={() => setSelectedProtest(null)}
|
||||
onAccept={handleAcceptProtest}
|
||||
onReject={handleRejectProtest}
|
||||
/>
|
||||
)}
|
||||
|
||||
{selectedProtest && (
|
||||
<ReviewProtestModal
|
||||
protest={selectedProtest}
|
||||
onClose={() => setSelectedProtest(null)}
|
||||
onAccept={handleAcceptProtest}
|
||||
onReject={handleRejectProtest}
|
||||
/>
|
||||
)}
|
||||
|
||||
{showQuickPenaltyModal && stewardingData && (
|
||||
<QuickPenaltyModal
|
||||
drivers={stewardingData.allDrivers}
|
||||
onClose={() => setShowQuickPenaltyModal(false)}
|
||||
adminId={currentDriverId}
|
||||
races={stewardingData.racesWithData.map(r => ({ id: r.race.id, track: r.race.track, scheduledAt: r.race.scheduledAt }))}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
{showQuickPenaltyModal && stewardingData && (
|
||||
<QuickPenaltyModal
|
||||
drivers={stewardingData.allDrivers}
|
||||
onClose={() => setShowQuickPenaltyModal(false)}
|
||||
adminId={currentDriverId || ''}
|
||||
races={stewardingData.racesWithData.map(r => ({ id: r.race.id, track: r.race.track, scheduledAt: r.race.scheduledAt }))}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}}
|
||||
</StateContainer>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user