565 lines
28 KiB
TypeScript
565 lines
28 KiB
TypeScript
'use client';
|
|
|
|
import { Box } from '@/ui/Box';
|
|
import { Button } from '@/ui/Button';
|
|
import { Stack } from '@/ui/Stack';
|
|
import { Text } from '@/ui/Text';
|
|
import { Icon } from '@/ui/Icon';
|
|
import { Card } from '@/ui/Card';
|
|
import { Container } from '@/ui/Container';
|
|
import { Heading } from '@/ui/Heading';
|
|
import { Link as UILink } from '@/ui/Link';
|
|
import { Grid } from '@/ui/Grid';
|
|
import { GridItem } from '@/ui/GridItem';
|
|
import {
|
|
AlertCircle,
|
|
AlertTriangle,
|
|
ArrowLeft,
|
|
Calendar,
|
|
CheckCircle,
|
|
ChevronDown,
|
|
Clock,
|
|
ExternalLink,
|
|
Flag,
|
|
Gavel,
|
|
Grid3x3,
|
|
MapPin,
|
|
MessageCircle,
|
|
Send,
|
|
Shield,
|
|
ShieldAlert,
|
|
TrendingDown,
|
|
User,
|
|
Video,
|
|
XCircle,
|
|
type LucideIcon
|
|
} from 'lucide-react';
|
|
import { routes } from '@/lib/routing/RouteConfig';
|
|
import { TemplateProps } from '@/lib/contracts/components/ComponentContracts';
|
|
import { ViewData } from '@/lib/contracts/view-data/ViewData';
|
|
|
|
interface ProtestDetailTemplateProps extends TemplateProps<ViewData> {
|
|
protestDetail: any;
|
|
leagueId: string;
|
|
showDecisionPanel: boolean;
|
|
setShowDecisionPanel: (show: boolean) => void;
|
|
decision: 'uphold' | 'dismiss' | null;
|
|
setDecision: (decision: 'uphold' | 'dismiss' | null) => void;
|
|
penaltyType: string;
|
|
setPenaltyType: (type: string) => void;
|
|
penaltyValue: number;
|
|
setPenaltyValue: (value: number) => void;
|
|
stewardNotes: string;
|
|
setStewardNotes: (notes: string) => void;
|
|
submitting: boolean;
|
|
newComment: string;
|
|
setNewComment: (comment: string) => void;
|
|
penaltyTypes: any[];
|
|
selectedPenalty: any;
|
|
onSubmitDecision: () => void;
|
|
onRequestDefense: () => void;
|
|
getStatusConfig: (status: string) => any;
|
|
}
|
|
|
|
export function ProtestDetailTemplate({
|
|
protestDetail,
|
|
leagueId,
|
|
showDecisionPanel,
|
|
setShowDecisionPanel,
|
|
decision,
|
|
setDecision,
|
|
penaltyType,
|
|
setPenaltyType,
|
|
penaltyValue,
|
|
setPenaltyValue,
|
|
stewardNotes,
|
|
setStewardNotes,
|
|
submitting,
|
|
newComment,
|
|
setNewComment,
|
|
penaltyTypes,
|
|
selectedPenalty,
|
|
onSubmitDecision,
|
|
onRequestDefense,
|
|
getStatusConfig,
|
|
}: ProtestDetailTemplateProps) {
|
|
if (!protestDetail) return null;
|
|
|
|
const protest = protestDetail.protest || protestDetail;
|
|
const race = protestDetail.race;
|
|
const protestingDriver = protestDetail.protestingDriver;
|
|
const accusedDriver = protestDetail.accusedDriver;
|
|
|
|
const statusConfig = getStatusConfig(protest.status);
|
|
const StatusIcon = statusConfig.icon;
|
|
const isPending = protest.status === 'pending';
|
|
const submittedAt = protest.submittedAt || protestDetail.submittedAt;
|
|
const daysSinceFiled = Math.floor((Date.now() - new Date(submittedAt).getTime()) / (1000 * 60 * 60 * 24));
|
|
|
|
return (
|
|
<Box minHeight="100vh">
|
|
<Container size="lg">
|
|
<Box paddingY={8}>
|
|
{/* Compact Header */}
|
|
<Box mb={6}>
|
|
<Stack direction="row" align="center" gap={3} mb={4}>
|
|
<UILink href={routes.league.stewarding(leagueId)}>
|
|
<Icon icon={ArrowLeft} size={5} color="text-gray-400" />
|
|
</UILink>
|
|
<Stack direction="row" align="center" gap={3} flexGrow={1}>
|
|
<Heading level={1}>Protest Review</Heading>
|
|
<Box display="flex" alignItems="center" gap={1.5} px={2.5} py={1} rounded="full" fontSize="0.75rem" weight="medium" border bg={statusConfig.bg} color={statusConfig.color} borderColor={statusConfig.borderColor}>
|
|
<Icon icon={StatusIcon} size={3} />
|
|
<Text>{statusConfig.label}</Text>
|
|
</Box>
|
|
{daysSinceFiled > 2 && isPending && (
|
|
<Box display="flex" alignItems="center" gap={1} px={2} py={0.5} fontSize="0.75rem" weight="medium" bg="bg-red-500/20" color="text-red-400" rounded="full">
|
|
<Icon icon={AlertTriangle} size={3} />
|
|
<Text>{daysSinceFiled}d old</Text>
|
|
</Box>
|
|
)}
|
|
</Stack>
|
|
</Stack>
|
|
</Box>
|
|
|
|
{/* Main Layout: Feed + Sidebar */}
|
|
<Grid cols={12} gap={6}>
|
|
{/* Left Sidebar - Incident Info */}
|
|
<GridItem colSpan={{ base: 12, lg: 3 }}>
|
|
<Stack gap={4}>
|
|
{/* Drivers Involved */}
|
|
<Card>
|
|
<Box p={4}>
|
|
<Heading level={3} fontSize="xs" weight="semibold" color="text-gray-500" mb={3}>Parties Involved</Heading>
|
|
|
|
<Stack gap={3}>
|
|
{/* Protesting Driver */}
|
|
<UILink href={routes.driver.detail(protestingDriver?.id || '')} block>
|
|
<Box display="flex" alignItems="center" gap={3} p={3} rounded="lg" bg="bg-deep-graphite" border borderColor="border-charcoal-outline" hoverBorderColor="border-blue-500/50" hoverBg="bg-blue-500/5" transition cursor="pointer">
|
|
<Box w={10} h={10} rounded="full" bg="bg-blue-500/20" display="flex" alignItems="center" justifyContent="center" flexShrink={0}>
|
|
<Icon icon={User} size={5} color="text-blue-400" />
|
|
</Box>
|
|
<Box flexGrow={1} minWidth="0">
|
|
<Text size="xs" color="text-blue-400" weight="medium" block>Protesting</Text>
|
|
<Text size="sm" weight="semibold" color="text-white" truncate block>{protestingDriver?.name || 'Unknown'}</Text>
|
|
</Box>
|
|
<Icon icon={ExternalLink} size={3} color="text-gray-500" />
|
|
</Box>
|
|
</UILink>
|
|
|
|
{/* Accused Driver */}
|
|
<UILink href={routes.driver.detail(accusedDriver?.id || '')} block>
|
|
<Box display="flex" alignItems="center" gap={3} p={3} rounded="lg" bg="bg-deep-graphite" border borderColor="border-charcoal-outline" hoverBorderColor="border-orange-500/50" hoverBg="bg-orange-500/5" transition cursor="pointer">
|
|
<Box w={10} h={10} rounded="full" bg="bg-orange-500/20" display="flex" alignItems="center" justifyContent="center" flexShrink={0}>
|
|
<Icon icon={User} size={5} color="text-orange-400" />
|
|
</Box>
|
|
<Box flexGrow={1} minWidth="0">
|
|
<Text size="xs" color="text-orange-400" weight="medium" block>Accused</Text>
|
|
<Text size="sm" weight="semibold" color="text-white" truncate block>{accusedDriver?.name || 'Unknown'}</Text>
|
|
</Box>
|
|
<Icon icon={ExternalLink} size={3} color="text-gray-500" />
|
|
</Box>
|
|
</UILink>
|
|
</Stack>
|
|
</Box>
|
|
</Card>
|
|
|
|
{/* Race Info */}
|
|
<Card>
|
|
<Box p={4}>
|
|
<Heading level={3} fontSize="xs" weight="semibold" color="text-gray-500" mb={3}>Race Details</Heading>
|
|
|
|
<Box marginBottom={3}>
|
|
<UILink
|
|
href={routes.race.detail(race?.id || '')}
|
|
block
|
|
>
|
|
<Box p={3} rounded="lg" bg="bg-deep-graphite" border borderColor="border-charcoal-outline" hoverBorderColor="border-primary-blue/50" hoverBg="bg-primary-blue/5" transition>
|
|
<Box display="flex" alignItems="center" justifyContent="between">
|
|
<Text size="sm" weight="medium" color="text-white">{race?.name || 'Unknown Race'}</Text>
|
|
<Icon icon={ExternalLink} size={3} color="text-gray-500" />
|
|
</Box>
|
|
</Box>
|
|
</UILink>
|
|
</Box>
|
|
|
|
<Stack gap={2}>
|
|
<Box display="flex" alignItems="center" gap={2}>
|
|
<Icon icon={MapPin} size={4} color="text-gray-500" />
|
|
<Text size="sm" color="text-gray-300">{race?.name || 'Unknown Track'}</Text>
|
|
</Box>
|
|
<Box display="flex" alignItems="center" gap={2}>
|
|
<Icon icon={Calendar} size={4} color="text-gray-500" />
|
|
<Text size="sm" color="text-gray-300">{race?.formattedDate || (race?.scheduledAt ? new Date(race.scheduledAt).toLocaleDateString() : 'Unknown Date')}</Text>
|
|
</Box>
|
|
{protest.incident?.lap && (
|
|
<Box display="flex" alignItems="center" gap={2}>
|
|
<Icon icon={Flag} size={4} color="text-gray-500" />
|
|
<Text size="sm" color="text-gray-300">Lap {protest.incident.lap}</Text>
|
|
</Box>
|
|
)}
|
|
</Stack>
|
|
</Box>
|
|
</Card>
|
|
|
|
{protest.proofVideoUrl && (
|
|
<Card>
|
|
<Box p={4}>
|
|
<Heading level={3} fontSize="xs" weight="semibold" color="text-gray-500" mb={3}>Evidence</Heading>
|
|
<UILink
|
|
href={protest.proofVideoUrl}
|
|
target="_blank"
|
|
rel="noopener noreferrer"
|
|
block
|
|
>
|
|
<Box display="flex" alignItems="center" gap={2} p={3} rounded="lg" bg="bg-primary-blue/10" border borderColor="border-primary-blue/20" color="text-primary-blue" hoverBg="bg-primary-blue/20" transition>
|
|
<Icon icon={Video} size={4} />
|
|
<Text size="sm" weight="medium" flexGrow={1}>Watch Video</Text>
|
|
<Icon icon={ExternalLink} size={3} />
|
|
</Box>
|
|
</UILink>
|
|
</Box>
|
|
</Card>
|
|
)}
|
|
|
|
{/* Quick Stats */}
|
|
<Card>
|
|
<Box p={4}>
|
|
<Heading level={3} fontSize="xs" weight="semibold" color="text-gray-500" mb={3}>Timeline</Heading>
|
|
<Stack gap={2}>
|
|
<Box display="flex" justifyContent="between">
|
|
<Text size="sm" color="text-gray-500">Filed</Text>
|
|
<Text size="sm" color="text-gray-300">{new Date(submittedAt).toLocaleDateString()}</Text>
|
|
</Box>
|
|
<Box display="flex" justifyContent="between">
|
|
<Text size="sm" color="text-gray-500">Age</Text>
|
|
<Text size="sm" color={daysSinceFiled > 2 ? 'text-red-400' : 'text-gray-300'}>{daysSinceFiled} days</Text>
|
|
</Box>
|
|
{protest.reviewedAt && (
|
|
<Box display="flex" justifyContent="between">
|
|
<Text size="sm" color="text-gray-500">Resolved</Text>
|
|
<Text size="sm" color="text-gray-300">{new Date(protest.reviewedAt).toLocaleDateString()}</Text>
|
|
</Box>
|
|
)}
|
|
</Stack>
|
|
</Box>
|
|
</Card>
|
|
</Stack>
|
|
</GridItem>
|
|
|
|
{/* Center - Discussion Feed */}
|
|
<GridItem colSpan={12} lgSpan={6}>
|
|
<Stack gap={4}>
|
|
{/* Timeline / Feed */}
|
|
<Card>
|
|
<Box borderBottom borderColor="border-charcoal-outline" bg="bg-iron-gray/30" p={4}>
|
|
<Heading level={2}>Discussion</Heading>
|
|
</Box>
|
|
|
|
<Stack gap={0}>
|
|
{/* Initial Protest Filing */}
|
|
<Box p={4}>
|
|
<Box display="flex" gap={3}>
|
|
<Box w={10} h={10} rounded="full" bg="bg-blue-500/20" display="flex" alignItems="center" justifyContent="center" flexShrink={0}>
|
|
<Icon icon={AlertCircle} size={5} color="text-blue-400" />
|
|
</Box>
|
|
<Box flexGrow={1} minWidth="0">
|
|
<Box display="flex" alignItems="center" gap={2} mb={1}>
|
|
<Text weight="semibold" color="text-white" size="sm">{protestingDriver?.name || 'Unknown'}</Text>
|
|
<Text size="xs" color="text-blue-400" weight="medium">filed protest</Text>
|
|
<Text size="xs" color="text-gray-500">•</Text>
|
|
<Text size="xs" color="text-gray-500">{new Date(submittedAt).toLocaleString()}</Text>
|
|
</Box>
|
|
|
|
<Box bg="bg-deep-graphite" rounded="lg" p={4} border borderColor="border-charcoal-outline">
|
|
<Text size="sm" color="text-gray-300" block mb={3}>{protest.description || protestDetail.incident?.description}</Text>
|
|
|
|
{(protest.comment || protestDetail.comment) && (
|
|
<Box mt={3} pt={3} borderTop borderColor="border-charcoal-outline/50">
|
|
<Text size="xs" color="text-gray-500" block mb={1}>Additional details:</Text>
|
|
<Text size="sm" color="text-gray-400">{protest.comment || protestDetail.comment}</Text>
|
|
</Box>
|
|
)}
|
|
</Box>
|
|
</Box>
|
|
</Box>
|
|
</Box>
|
|
|
|
{/* Defense placeholder */}
|
|
{protest.status === 'awaiting_defense' && (
|
|
<Box p={4} bg="bg-purple-500/5">
|
|
<Box display="flex" gap={3}>
|
|
<Box w={10} h={10} rounded="full" bg="bg-purple-500/20" display="flex" alignItems="center" justifyContent="center" flexShrink={0}>
|
|
<Icon icon={MessageCircle} size={5} color="text-purple-400" />
|
|
</Box>
|
|
<Box flexGrow={1}>
|
|
<Text size="sm" color="text-purple-400" weight="medium" block mb={1}>Defense Requested</Text>
|
|
<Text size="sm" color="text-gray-400">Waiting for {accusedDriver?.name || 'the accused driver'} to submit their defense...</Text>
|
|
</Box>
|
|
</Box>
|
|
</Box>
|
|
)}
|
|
|
|
{/* Decision (if resolved) */}
|
|
{(protest.status === 'upheld' || protest.status === 'dismissed') && protest.decisionNotes && (
|
|
<Box p={4} bg={protest.status === 'upheld' ? 'bg-red-500/5' : 'bg-gray-500/5'}>
|
|
<Box display="flex" gap={3}>
|
|
<Box w={10} h={10} rounded="full" display="flex" alignItems="center" justifyContent="center" flexShrink={0} bg={protest.status === 'upheld' ? 'bg-red-500/20' : 'bg-gray-500/20'}>
|
|
<Icon icon={Gavel} size={5} color={protest.status === 'upheld' ? 'text-red-400' : 'text-gray-400'} />
|
|
</Box>
|
|
<Box flexGrow={1} minWidth="0">
|
|
<Box display="flex" alignItems="center" gap={2} mb={1}>
|
|
<Text weight="semibold" color="text-white" size="sm">Steward Decision</Text>
|
|
<Text size="xs" weight="medium" color={protest.status === 'upheld' ? 'text-red-400' : 'text-gray-400'}>
|
|
{protest.status === 'upheld' ? 'Protest Upheld' : 'Protest Dismissed'}
|
|
</Text>
|
|
{protest.reviewedAt && (
|
|
<>
|
|
<Text size="xs" color="text-gray-500">•</Text>
|
|
<Text size="xs" color="text-gray-500">{new Date(protest.reviewedAt).toLocaleString()}</Text>
|
|
</>
|
|
)}
|
|
</Box>
|
|
|
|
<Box rounded="lg" p={4} border bg={protest.status === 'upheld' ? 'bg-red-500/10' : 'bg-gray-500/10'} borderColor={protest.status === 'upheld' ? 'border-red-500/20' : 'border-gray-500/20'}>
|
|
<Text size="sm" color="text-gray-300">{protest.decisionNotes}</Text>
|
|
</Box>
|
|
</Box>
|
|
</Box>
|
|
</Box>
|
|
)}
|
|
</Stack>
|
|
|
|
{/* Add Comment */}
|
|
{isPending && (
|
|
<Box p={4} borderTop borderColor="border-charcoal-outline" bg="bg-iron-gray/20">
|
|
<Box display="flex" gap={3}>
|
|
<Box w={10} h={10} rounded="full" bg="bg-iron-gray" display="flex" alignItems="center" justifyContent="center" flexShrink={0}>
|
|
<Icon icon={User} size={5} color="text-gray-500" />
|
|
</Box>
|
|
<Box flexGrow={1}>
|
|
<Box as="textarea"
|
|
value={newComment}
|
|
onChange={(e: React.ChangeEvent<HTMLTextAreaElement>) => setNewComment(e.target.value)}
|
|
placeholder="Add a comment or request more information..."
|
|
style={{ height: '4rem' }}
|
|
w="full"
|
|
px={4}
|
|
py={3}
|
|
bg="bg-deep-graphite"
|
|
border
|
|
borderColor="border-charcoal-outline"
|
|
rounded="lg"
|
|
color="text-white"
|
|
fontSize="sm"
|
|
/>
|
|
<Box display="flex" justifyContent="end" mt={2}>
|
|
<Button variant="secondary" disabled={!newComment.trim()}>
|
|
<Icon icon={Send} size={3} style={{ marginRight: '0.25rem' }} />
|
|
Comment
|
|
</Button>
|
|
</Box>
|
|
</Box>
|
|
</Box>
|
|
</Box>
|
|
)}
|
|
</Card>
|
|
</Stack>
|
|
</GridItem>
|
|
|
|
{/* Right Sidebar - Actions */}
|
|
<GridItem colSpan={12} lgSpan={3}>
|
|
<Stack gap={4}>
|
|
{isPending && (
|
|
<>
|
|
{/* Quick Actions */}
|
|
<Card>
|
|
<Box p={4}>
|
|
<Heading level={3} fontSize="xs" weight="semibold" color="text-gray-500" mb={3}>Actions</Heading>
|
|
|
|
<Stack gap={2}>
|
|
<Button
|
|
variant="secondary"
|
|
fullWidth
|
|
onClick={onRequestDefense}
|
|
>
|
|
<Stack direction="row" align="center" gap={2}>
|
|
<Icon icon={MessageCircle} size={4} />
|
|
<Text>Request Defense</Text>
|
|
</Stack>
|
|
</Button>
|
|
|
|
<Button
|
|
variant="primary"
|
|
fullWidth
|
|
onClick={() => setShowDecisionPanel(!showDecisionPanel)}
|
|
>
|
|
<Stack direction="row" align="center" gap={2} fullWidth>
|
|
<Icon icon={Gavel} size={4} />
|
|
<Text>Make Decision</Text>
|
|
<Box ml="auto" transition style={{ transform: showDecisionPanel ? 'rotate(180deg)' : 'none' }}>
|
|
<Icon icon={ChevronDown} size={4} />
|
|
</Box>
|
|
</Stack>
|
|
</Button>
|
|
</Stack>
|
|
</Box>
|
|
</Card>
|
|
|
|
{/* Decision Panel */}
|
|
{showDecisionPanel && (
|
|
<Card>
|
|
<Box p={4}>
|
|
<Heading level={3} fontSize="xs" weight="semibold" color="text-gray-500" mb={3}>Stewarding Decision</Heading>
|
|
|
|
{/* Decision Selection */}
|
|
<Grid cols={2} gap={2} mb={4}>
|
|
<Box padding={3} border borderColor={decision === 'uphold' ? 'border-racing-red' : 'border-charcoal-outline'} bg={decision === 'uphold' ? 'bg-racing-red/10' : 'transparent'} rounded="lg">
|
|
<Button
|
|
variant="ghost"
|
|
onClick={() => setDecision('uphold')}
|
|
fullWidth
|
|
>
|
|
<Stack align="center" gap={1}>
|
|
<Icon icon={CheckCircle} size={5} color={decision === 'uphold' ? 'text-red-400' : 'text-gray-500'} />
|
|
<Text size="xs" weight="medium" color={decision === 'uphold' ? 'text-red-400' : 'text-gray-400'}>Uphold</Text>
|
|
</Stack>
|
|
</Button>
|
|
</Box>
|
|
<Box padding={3} border borderColor={decision === 'dismiss' ? 'border-gray-500' : 'border-charcoal-outline'} bg={decision === 'dismiss' ? 'bg-gray-500/10' : 'transparent'} rounded="lg">
|
|
<Button
|
|
variant="ghost"
|
|
onClick={() => setDecision('dismiss')}
|
|
fullWidth
|
|
>
|
|
<Stack align="center" gap={1}>
|
|
<Icon icon={XCircle} size={5} color={decision === 'dismiss' ? 'text-gray-300' : 'text-gray-500'} />
|
|
<Text size="xs" weight="medium" color={decision === 'dismiss' ? 'text-gray-300' : 'text-gray-400'}>Dismiss</Text>
|
|
</Stack>
|
|
</Button>
|
|
</Box>
|
|
</Grid>
|
|
|
|
{/* Penalty Selection (if upholding) */}
|
|
{decision === 'uphold' && (
|
|
<Box mb={4}>
|
|
<Text as="label" size="xs" weight="medium" color="text-gray-400" block mb={2}>Penalty Type</Text>
|
|
|
|
{penaltyTypes.length === 0 ? (
|
|
<Text size="xs" color="text-gray-500">
|
|
Loading penalty types...
|
|
</Text>
|
|
) : (
|
|
<>
|
|
<Grid cols={2} gap={2}>
|
|
{penaltyTypes.map((penalty: any) => {
|
|
const Icon = penalty.icon;
|
|
const isSelected = penaltyType === penalty.type;
|
|
return (
|
|
<Box key={penalty.type} padding={2} border borderColor={isSelected ? undefined : 'border-charcoal-outline'} bg={isSelected ? undefined : 'bg-iron-gray/30'} rounded="lg">
|
|
<Button
|
|
variant="ghost"
|
|
onClick={() => {
|
|
setPenaltyType(penalty.type);
|
|
setPenaltyValue(penalty.defaultValue);
|
|
}}
|
|
fullWidth
|
|
title={penalty.description}
|
|
>
|
|
<Stack align="start" gap={0.5}>
|
|
<Icon icon={Icon} size={3.5} color={isSelected ? penalty.color : 'text-gray-500'} />
|
|
<Text size="xs" weight="medium" fontSize="10px" color={isSelected ? penalty.color : 'text-gray-500'}>
|
|
{penalty.label}
|
|
</Text>
|
|
</Stack>
|
|
</Button>
|
|
</Box>
|
|
);
|
|
})}
|
|
</Grid>
|
|
|
|
{selectedPenalty?.requiresValue && (
|
|
<Box mt={3}>
|
|
<Text as="label" size="xs" weight="medium" color="text-gray-400" block mb={1}>
|
|
Value ({selectedPenalty.valueLabel})
|
|
</Text>
|
|
<Box as="input"
|
|
type="number"
|
|
value={penaltyValue}
|
|
onChange={(e: React.ChangeEvent<HTMLInputElement>) => setPenaltyValue(Number(e.target.value))}
|
|
min="1"
|
|
w="full"
|
|
px={3}
|
|
py={2}
|
|
bg="bg-deep-graphite"
|
|
border
|
|
borderColor="border-charcoal-outline"
|
|
rounded="lg"
|
|
color="text-white"
|
|
fontSize="sm"
|
|
/>
|
|
</Box>
|
|
)}
|
|
</>
|
|
)}
|
|
</Box>
|
|
)}
|
|
|
|
{/* Steward Notes */}
|
|
<Box mb={4}>
|
|
<Text as="label" size="xs" weight="medium" color="text-gray-400" block mb={1}>Decision Reasoning *</Text>
|
|
<Box as="textarea"
|
|
value={stewardNotes}
|
|
onChange={(e: React.ChangeEvent<HTMLTextAreaElement>) => setStewardNotes(e.target.value)}
|
|
placeholder="Explain your decision..."
|
|
style={{ height: '8rem' }}
|
|
w="full"
|
|
px={3}
|
|
py={2}
|
|
bg="bg-deep-graphite"
|
|
border
|
|
borderColor="border-charcoal-outline"
|
|
rounded="lg"
|
|
color="text-white"
|
|
fontSize="sm"
|
|
/>
|
|
</Box>
|
|
|
|
{/* Submit */}
|
|
<Button
|
|
variant="primary"
|
|
fullWidth
|
|
onClick={onSubmitDecision}
|
|
disabled={!decision || !stewardNotes.trim() || submitting}
|
|
>
|
|
{submitting ? 'Submitting...' : 'Submit Decision'}
|
|
</Button>
|
|
</Box>
|
|
</Card>
|
|
)}
|
|
</>
|
|
)}
|
|
|
|
{/* Already Resolved Info */}
|
|
{!isPending && (
|
|
<Card>
|
|
<Box p={4} textAlign="center">
|
|
<Box py={4} color={protest.status === 'upheld' ? 'text-red-400' : 'text-gray-400'}>
|
|
<Icon icon={Gavel} size={8} style={{ margin: '0 auto 0.5rem auto' }} />
|
|
<Text weight="semibold" block>Case Closed</Text>
|
|
<Text size="xs" color="text-gray-500" block mt={1}>
|
|
{protest.status === 'upheld' ? 'Protest was upheld' : 'Protest was dismissed'}
|
|
</Text>
|
|
</Box>
|
|
</Box>
|
|
</Card>
|
|
)}
|
|
</Stack>
|
|
</GridItem>
|
|
</Grid>
|
|
</Box>
|
|
</Container>
|
|
</Box>
|
|
);
|
|
}
|