website refactor
This commit is contained in:
@@ -1,12 +1,19 @@
|
||||
"use client";
|
||||
|
||||
import { useMemo, useState } from "react";
|
||||
import { usePenaltyTypesReference } from "@/lib/hooks/usePenaltyTypesReference";
|
||||
import { usePenaltyTypesReference } from "@/hooks/usePenaltyTypesReference";
|
||||
import type { PenaltyValueKindDTO } from "@/lib/types/PenaltyTypesReferenceDTO";
|
||||
import { ProtestViewModel } from "../../lib/view-models/ProtestViewModel";
|
||||
import Modal from "../ui/Modal";
|
||||
import Button from "../ui/Button";
|
||||
import Card from "../ui/Card";
|
||||
import { Modal } from "@/ui/Modal";
|
||||
import { Button } from "@/ui/Button";
|
||||
import { Card } from "@/ui/Card";
|
||||
import { Box } from "@/ui/Box";
|
||||
import { Text } from "@/ui/Text";
|
||||
import { Stack } from "@/ui/Stack";
|
||||
import { Heading } from "@/ui/Heading";
|
||||
import { Icon } from "@/ui/Icon";
|
||||
import { TextArea } from "@/ui/TextArea";
|
||||
import { Input } from "@/ui/Input";
|
||||
import {
|
||||
AlertCircle,
|
||||
Video,
|
||||
@@ -15,12 +22,12 @@ import {
|
||||
TrendingDown,
|
||||
CheckCircle,
|
||||
XCircle,
|
||||
FileText,
|
||||
AlertTriangle,
|
||||
ShieldAlert,
|
||||
Ban,
|
||||
DollarSign,
|
||||
FileWarning,
|
||||
type LucideIcon,
|
||||
} from "lucide-react";
|
||||
|
||||
type PenaltyType = string;
|
||||
@@ -50,6 +57,27 @@ export function ReviewProtestModal({
|
||||
const [submitting, setSubmitting] = useState(false);
|
||||
const [showConfirmation, setShowConfirmation] = useState(false);
|
||||
|
||||
const { data: penaltyTypesReference, isLoading: penaltyTypesLoading } = usePenaltyTypesReference();
|
||||
|
||||
const penaltyOptions = useMemo(() => {
|
||||
const refs = penaltyTypesReference?.penaltyTypes ?? [];
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
return refs.map((ref: any) => ({
|
||||
type: ref.type as PenaltyType,
|
||||
name: getPenaltyName(ref.type),
|
||||
requiresValue: ref.requiresValue,
|
||||
valueLabel: getPenaltyValueLabel(ref.valueKind),
|
||||
defaultValue: getPenaltyDefaultValue(ref.type, ref.valueKind),
|
||||
Icon: getPenaltyIcon(ref.type),
|
||||
colorClass: getPenaltyColor(ref.type),
|
||||
}));
|
||||
}, [penaltyTypesReference]);
|
||||
|
||||
const selectedPenalty = useMemo(() => {
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
return penaltyOptions.find((p: any) => p.type === penaltyType);
|
||||
}, [penaltyOptions, penaltyType]);
|
||||
|
||||
if (!protest) return null;
|
||||
|
||||
const handleSubmit = async () => {
|
||||
@@ -178,63 +206,46 @@ export function ReviewProtestModal({
|
||||
}
|
||||
};
|
||||
|
||||
const { data: penaltyTypesReference, isLoading: penaltyTypesLoading } = usePenaltyTypesReference();
|
||||
|
||||
const penaltyOptions = useMemo(() => {
|
||||
const refs = penaltyTypesReference?.penaltyTypes ?? [];
|
||||
return refs.map((ref) => ({
|
||||
type: ref.type as PenaltyType,
|
||||
name: getPenaltyName(ref.type),
|
||||
requiresValue: ref.requiresValue,
|
||||
valueLabel: getPenaltyValueLabel(ref.valueKind),
|
||||
defaultValue: getPenaltyDefaultValue(ref.type, ref.valueKind),
|
||||
Icon: getPenaltyIcon(ref.type),
|
||||
colorClass: getPenaltyColor(ref.type),
|
||||
}));
|
||||
}, [penaltyTypesReference]);
|
||||
|
||||
const selectedPenalty = useMemo(() => {
|
||||
return penaltyOptions.find((p) => p.type === penaltyType);
|
||||
}, [penaltyOptions, penaltyType]);
|
||||
|
||||
if (showConfirmation) {
|
||||
return (
|
||||
<Modal title="Confirm Decision" isOpen={true} onOpenChange={() => setShowConfirmation(false)}>
|
||||
<div className="p-6 space-y-6">
|
||||
<div className="text-center space-y-4">
|
||||
{decision === "accept" ? (
|
||||
<div className="flex justify-center">
|
||||
<div className="h-16 w-16 rounded-full bg-orange-500/20 flex items-center justify-center">
|
||||
<AlertCircle className="h-8 w-8 text-orange-400" />
|
||||
</div>
|
||||
</div>
|
||||
) : (
|
||||
<div className="flex justify-center">
|
||||
<div className="h-16 w-16 rounded-full bg-gray-500/20 flex items-center justify-center">
|
||||
<XCircle className="h-8 w-8 text-gray-400" />
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
<div>
|
||||
<h3 className="text-xl font-bold text-white">Confirm Decision</h3>
|
||||
<p className="text-gray-400 mt-2">
|
||||
{decision === "accept"
|
||||
? (selectedPenalty?.requiresValue
|
||||
? `Issue ${penaltyValue} ${selectedPenalty.valueLabel} penalty?`
|
||||
: `Issue ${selectedPenalty?.name ?? penaltyType} penalty?`)
|
||||
: "Reject this protest?"}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
<Stack gap={6} p={6}>
|
||||
<Box textAlign="center">
|
||||
<Stack gap={4}>
|
||||
{decision === "accept" ? (
|
||||
<Box display="flex" justifyContent="center">
|
||||
<Box h="16" w="16" rounded="full" bg="bg-orange-500/20" display="flex" alignItems="center" justifyContent="center">
|
||||
<Icon icon={AlertCircle} size={8} color="text-orange-400" />
|
||||
</Box>
|
||||
</Box>
|
||||
) : (
|
||||
<Box display="flex" justifyContent="center">
|
||||
<Box h="16" w="16" rounded="full" bg="bg-gray-500/20" display="flex" alignItems="center" justifyContent="center">
|
||||
<Icon icon={XCircle} size={8} color="text-gray-400" />
|
||||
</Box>
|
||||
</Box>
|
||||
)}
|
||||
<Box>
|
||||
<Heading level={3} fontSize="xl" weight="bold" color="text-white">Confirm Decision</Heading>
|
||||
<Text color="text-gray-400" mt={2} block>
|
||||
{decision === "accept"
|
||||
? (selectedPenalty?.requiresValue
|
||||
? `Issue ${penaltyValue} ${selectedPenalty.valueLabel} penalty?`
|
||||
: `Issue ${selectedPenalty?.name ?? penaltyType} penalty?`)
|
||||
: "Reject this protest?"}
|
||||
</Text>
|
||||
</Box>
|
||||
</Stack>
|
||||
</Box>
|
||||
|
||||
<Card className="p-4 bg-gray-800/50">
|
||||
<p className="text-sm text-gray-300">{stewardNotes}</p>
|
||||
<Card p={4} bg="bg-gray-800/50">
|
||||
<Text size="sm" color="text-gray-300" block>{stewardNotes}</Text>
|
||||
</Card>
|
||||
|
||||
<div className="flex gap-3">
|
||||
<Box display="flex" gap={3}>
|
||||
<Button
|
||||
variant="secondary"
|
||||
className="flex-1"
|
||||
fullWidth
|
||||
onClick={() => setShowConfirmation(false)}
|
||||
disabled={submitting}
|
||||
>
|
||||
@@ -242,189 +253,207 @@ export function ReviewProtestModal({
|
||||
</Button>
|
||||
<Button
|
||||
variant="primary"
|
||||
className="flex-1"
|
||||
fullWidth
|
||||
onClick={handleSubmit}
|
||||
disabled={submitting}
|
||||
>
|
||||
{submitting ? "Submitting..." : "Confirm Decision"}
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</Box>
|
||||
</Stack>
|
||||
</Modal>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<Modal title="Review Protest" isOpen={true} onOpenChange={onClose}>
|
||||
<div className="p-6 space-y-6">
|
||||
<div className="flex items-start gap-4">
|
||||
<div className="h-12 w-12 rounded-full bg-orange-500/20 flex items-center justify-center flex-shrink-0">
|
||||
<AlertCircle className="h-6 w-6 text-orange-400" />
|
||||
</div>
|
||||
<div className="flex-1">
|
||||
<h2 className="text-2xl font-bold text-white">Review Protest</h2>
|
||||
<p className="text-gray-400 mt-1">
|
||||
<Stack gap={6} p={6}>
|
||||
<Box display="flex" alignItems="start" gap={4}>
|
||||
<Box h="12" w="12" rounded="full" bg="bg-orange-500/20" display="flex" alignItems="center" justifyContent="center" flexShrink={0}>
|
||||
<Icon icon={AlertCircle} size={6} color="text-orange-400" />
|
||||
</Box>
|
||||
<Box flexGrow={1}>
|
||||
<Heading level={2} fontSize="2xl" weight="bold" color="text-white">Review Protest</Heading>
|
||||
<Text color="text-gray-400" mt={1} block>
|
||||
Protest #{protest.id.substring(0, 8)}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</Text>
|
||||
</Box>
|
||||
</Box>
|
||||
|
||||
<div className="space-y-4">
|
||||
<Card className="p-4 bg-gray-800/50">
|
||||
<div className="space-y-3">
|
||||
<div className="flex items-center justify-between text-sm">
|
||||
<span className="text-gray-400">Filed Date</span>
|
||||
<span className="text-white font-medium">
|
||||
<Stack gap={4}>
|
||||
<Card p={4} bg="bg-gray-800/50">
|
||||
<Stack gap={3}>
|
||||
<Box display="flex" alignItems="center" justifyContent="between">
|
||||
<Text size="sm" color="text-gray-400">Filed Date</Text>
|
||||
<Text size="sm" color="text-white" weight="medium">
|
||||
{new Date(protest.filedAt || protest.submittedAt).toLocaleString()}
|
||||
</span>
|
||||
</div>
|
||||
<div className="flex items-center justify-between text-sm">
|
||||
<span className="text-gray-400">Incident Lap</span>
|
||||
<span className="text-white font-medium">
|
||||
</Text>
|
||||
</Box>
|
||||
<Box display="flex" alignItems="center" justifyContent="between">
|
||||
<Text size="sm" color="text-gray-400">Incident Lap</Text>
|
||||
<Text size="sm" color="text-white" weight="medium">
|
||||
Lap {protest.incident?.lap || 'N/A'}
|
||||
</span>
|
||||
</div>
|
||||
<div className="flex items-center justify-between text-sm">
|
||||
<span className="text-gray-400">Status</span>
|
||||
<span className="px-2 py-1 rounded text-xs font-medium bg-orange-500/20 text-orange-400">
|
||||
{protest.status}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</Text>
|
||||
</Box>
|
||||
<Box display="flex" alignItems="center" justifyContent="between">
|
||||
<Text size="sm" color="text-gray-400">Status</Text>
|
||||
<Box as="span" px={2} py={1} rounded="sm" bg="bg-orange-500/20">
|
||||
<Text size="xs" weight="medium" color="text-orange-400">
|
||||
{protest.status}
|
||||
</Text>
|
||||
</Box>
|
||||
</Box>
|
||||
</Stack>
|
||||
</Card>
|
||||
|
||||
<div>
|
||||
<label className="text-sm font-medium text-gray-400 mb-2 block">
|
||||
<Box>
|
||||
<Text as="label" size="sm" weight="medium" color="text-gray-400" block mb={2}>
|
||||
Description
|
||||
</label>
|
||||
<Card className="p-4 bg-gray-800/50">
|
||||
<p className="text-gray-300">{protest.incident?.description || protest.description}</p>
|
||||
</Text>
|
||||
<Card p={4} bg="bg-gray-800/50">
|
||||
<Text color="text-gray-300" block>{protest.incident?.description || protest.description}</Text>
|
||||
</Card>
|
||||
</div>
|
||||
</Box>
|
||||
|
||||
{protest.comment && (
|
||||
<div>
|
||||
<label className="text-sm font-medium text-gray-400 mb-2 block">
|
||||
<Box>
|
||||
<Text as="label" size="sm" weight="medium" color="text-gray-400" block mb={2}>
|
||||
Additional Comment
|
||||
</label>
|
||||
<Card className="p-4 bg-gray-800/50">
|
||||
<p className="text-gray-300">{protest.comment}</p>
|
||||
</Text>
|
||||
<Card p={4} bg="bg-gray-800/50">
|
||||
<Text color="text-gray-300" block>{protest.comment}</Text>
|
||||
</Card>
|
||||
</div>
|
||||
</Box>
|
||||
)}
|
||||
|
||||
{protest.proofVideoUrl && (
|
||||
<div>
|
||||
<label className="text-sm font-medium text-gray-400 mb-2 block">
|
||||
<Box>
|
||||
<Text as="label" size="sm" weight="medium" color="text-gray-400" block mb={2}>
|
||||
Evidence
|
||||
</label>
|
||||
<Card className="p-4 bg-gray-800/50">
|
||||
<a
|
||||
</Text>
|
||||
<Card p={4} bg="bg-gray-800/50">
|
||||
<Box
|
||||
as="a"
|
||||
href={protest.proofVideoUrl}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className="flex items-center gap-2 text-orange-400 hover:text-orange-300 transition-colors"
|
||||
display="flex"
|
||||
alignItems="center"
|
||||
gap={2}
|
||||
color="text-orange-400"
|
||||
hoverTextColor="text-orange-300"
|
||||
transition
|
||||
>
|
||||
<Video className="h-4 w-4" />
|
||||
<span className="text-sm">View video evidence</span>
|
||||
</a>
|
||||
<Icon icon={Video} size={4} />
|
||||
<Text size="sm">View video evidence</Text>
|
||||
</Box>
|
||||
</Card>
|
||||
</div>
|
||||
</Box>
|
||||
)}
|
||||
</div>
|
||||
</Stack>
|
||||
|
||||
<div className="border-t border-gray-800 pt-6 space-y-4">
|
||||
<h3 className="text-lg font-semibold text-white">Stewarding Decision</h3>
|
||||
<Box borderTop borderColor="border-gray-800" pt={6}>
|
||||
<Stack gap={4}>
|
||||
<Heading level={3} fontSize="lg" weight="semibold" color="text-white">Stewarding Decision</Heading>
|
||||
|
||||
<div className="grid grid-cols-2 gap-3">
|
||||
<Button
|
||||
variant={decision === "accept" ? "primary" : "secondary"}
|
||||
className="flex items-center justify-center gap-2"
|
||||
onClick={() => setDecision("accept")}
|
||||
>
|
||||
<CheckCircle className="h-4 w-4" />
|
||||
Accept Protest
|
||||
</Button>
|
||||
<Button
|
||||
variant={decision === "reject" ? "primary" : "secondary"}
|
||||
className="flex items-center justify-center gap-2"
|
||||
onClick={() => setDecision("reject")}
|
||||
>
|
||||
<XCircle className="h-4 w-4" />
|
||||
Reject Protest
|
||||
</Button>
|
||||
</div>
|
||||
<Box display="grid" gridCols={2} gap={3}>
|
||||
<Button
|
||||
variant={decision === "accept" ? "primary" : "secondary"}
|
||||
fullWidth
|
||||
onClick={() => setDecision("accept")}
|
||||
>
|
||||
<Stack direction="row" align="center" gap={2} center>
|
||||
<Icon icon={CheckCircle} size={4} />
|
||||
Accept Protest
|
||||
</Stack>
|
||||
</Button>
|
||||
<Button
|
||||
variant={decision === "reject" ? "primary" : "secondary"}
|
||||
fullWidth
|
||||
onClick={() => setDecision("reject")}
|
||||
>
|
||||
<Stack direction="row" align="center" gap={2} center>
|
||||
<Icon icon={XCircle} size={4} />
|
||||
Reject Protest
|
||||
</Stack>
|
||||
</Button>
|
||||
</Box>
|
||||
|
||||
{decision === "accept" && (
|
||||
<div className="space-y-4">
|
||||
<div>
|
||||
<label className="text-sm font-medium text-gray-400 mb-2 block">
|
||||
Penalty Type
|
||||
</label>
|
||||
{decision === "accept" && (
|
||||
<Stack gap={4}>
|
||||
<Box>
|
||||
<Text as="label" size="sm" weight="medium" color="text-gray-400" block mb={2}>
|
||||
Penalty Type
|
||||
</Text>
|
||||
|
||||
{penaltyTypesLoading ? (
|
||||
<div className="text-sm text-gray-500">Loading penalty types…</div>
|
||||
) : (
|
||||
<div className="grid grid-cols-3 gap-2">
|
||||
{penaltyOptions.map(({ type, name, Icon, colorClass, defaultValue }) => {
|
||||
const isSelected = penaltyType === type;
|
||||
return (
|
||||
<button
|
||||
key={type}
|
||||
onClick={() => {
|
||||
setPenaltyType(type);
|
||||
setPenaltyValue(defaultValue);
|
||||
}}
|
||||
className={`p-3 rounded-lg border transition-all ${
|
||||
isSelected
|
||||
? `${colorClass} border-2`
|
||||
: "border-charcoal-outline hover:border-gray-600 bg-iron-gray/50"
|
||||
}`}
|
||||
>
|
||||
<Icon className={`h-5 w-5 mx-auto mb-1 ${isSelected ? "" : "text-gray-400"}`} />
|
||||
<p className={`text-xs font-medium ${isSelected ? "" : "text-gray-400"}`}>{name}</p>
|
||||
</button>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
{penaltyTypesLoading ? (
|
||||
<Text size="sm" color="text-gray-500">Loading penalty types…</Text>
|
||||
) : (
|
||||
<Box display="grid" gridCols={3} gap={2}>
|
||||
{penaltyOptions.map(({ type, name, Icon: PenaltyIcon, colorClass, defaultValue }: { type: string; name: string; Icon: LucideIcon; colorClass: string; defaultValue: number }) => {
|
||||
const isSelected = penaltyType === type;
|
||||
return (
|
||||
<Box
|
||||
key={type}
|
||||
as="button"
|
||||
onClick={() => {
|
||||
setPenaltyType(type);
|
||||
setPenaltyValue(defaultValue);
|
||||
}}
|
||||
p={3}
|
||||
rounded="lg"
|
||||
border
|
||||
borderWidth={isSelected ? "2px" : "1px"}
|
||||
transition
|
||||
borderColor={isSelected ? undefined : "border-charcoal-outline"}
|
||||
bg={isSelected ? undefined : "bg-iron-gray/50"}
|
||||
hoverBorderColor={!isSelected ? "border-gray-600" : undefined}
|
||||
// eslint-disable-next-line gridpilot-rules/component-classification
|
||||
className={isSelected ? colorClass : ""}
|
||||
>
|
||||
<Icon icon={PenaltyIcon} size={5} mx="auto" mb={1} color={isSelected ? undefined : "text-gray-400"} />
|
||||
<Text size="xs" weight="medium" color={isSelected ? undefined : "text-gray-400"} block textAlign="center">{name}</Text>
|
||||
</Box>
|
||||
);
|
||||
})}
|
||||
</Box>
|
||||
)}
|
||||
</Box>
|
||||
|
||||
{selectedPenalty?.requiresValue && (
|
||||
<Box>
|
||||
<Text as="label" size="sm" weight="medium" color="text-gray-400" block mb={2}>
|
||||
Penalty Value ({selectedPenalty.valueLabel})
|
||||
</Text>
|
||||
<Input
|
||||
type="number"
|
||||
value={penaltyValue}
|
||||
onChange={(e: React.ChangeEvent<HTMLInputElement>) => setPenaltyValue(Number(e.target.value))}
|
||||
min={1}
|
||||
/>
|
||||
</Box>
|
||||
)}
|
||||
</div>
|
||||
</Stack>
|
||||
)}
|
||||
|
||||
{selectedPenalty?.requiresValue && (
|
||||
<div>
|
||||
<label className="text-sm font-medium text-gray-400 mb-2 block">
|
||||
Penalty Value ({selectedPenalty.valueLabel})
|
||||
</label>
|
||||
<input
|
||||
type="number"
|
||||
value={penaltyValue}
|
||||
onChange={(e) => setPenaltyValue(Number(e.target.value))}
|
||||
min="1"
|
||||
className="w-full px-4 py-2 bg-gray-800 border border-gray-700 rounded-lg text-white focus:outline-none focus:border-orange-500"
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
<Box>
|
||||
<Text as="label" size="sm" weight="medium" color="text-gray-400" block mb={2}>
|
||||
Steward Notes *
|
||||
</Text>
|
||||
<TextArea
|
||||
value={stewardNotes}
|
||||
onChange={(e: React.ChangeEvent<HTMLTextAreaElement>) => setStewardNotes(e.target.value)}
|
||||
placeholder="Explain your decision and reasoning..."
|
||||
rows={4}
|
||||
/>
|
||||
</Box>
|
||||
</Stack>
|
||||
</Box>
|
||||
|
||||
<div>
|
||||
<label className="text-sm font-medium text-gray-400 mb-2 block">
|
||||
Steward Notes *
|
||||
</label>
|
||||
<textarea
|
||||
value={stewardNotes}
|
||||
onChange={(e) => setStewardNotes(e.target.value)}
|
||||
placeholder="Explain your decision and reasoning..."
|
||||
rows={4}
|
||||
className="w-full px-4 py-3 bg-gray-800 border border-gray-700 rounded-lg text-white placeholder-gray-500 focus:outline-none focus:border-orange-500 resize-none"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="flex gap-3 pt-4 border-t border-gray-800">
|
||||
<Box display="flex" gap={3} pt={4} borderTop borderColor="border-gray-800">
|
||||
<Button
|
||||
variant="secondary"
|
||||
className="flex-1"
|
||||
fullWidth
|
||||
onClick={onClose}
|
||||
disabled={submitting}
|
||||
>
|
||||
@@ -432,14 +461,14 @@ export function ReviewProtestModal({
|
||||
</Button>
|
||||
<Button
|
||||
variant="primary"
|
||||
className="flex-1"
|
||||
fullWidth
|
||||
onClick={() => setShowConfirmation(true)}
|
||||
disabled={!decision || !stewardNotes.trim() || submitting}
|
||||
>
|
||||
Submit Decision
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</Box>
|
||||
</Stack>
|
||||
</Modal>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user