This commit is contained in:
2025-12-13 18:39:20 +01:00
parent bb0497f429
commit e53af6a0e7
20 changed files with 762 additions and 503 deletions

View File

@@ -0,0 +1,57 @@
'use client';
import { useState } from 'react';
import { MoreVertical, Edit, Trash2 } from 'lucide-react';
import Button from '../ui/Button';
interface PenaltyCardMenuProps {
onEdit: () => void;
onVoid: () => void;
}
export default function PenaltyCardMenu({ onEdit, onVoid }: PenaltyCardMenuProps) {
const [isOpen, setIsOpen] = useState(false);
return (
<div className="relative">
<Button
variant="secondary"
className="p-2 w-8 h-8"
onClick={() => setIsOpen(!isOpen)}
>
<MoreVertical className="w-4 h-4" />
</Button>
{isOpen && (
<>
<div
className="fixed inset-0 z-10"
onClick={() => setIsOpen(false)}
/>
<div className="absolute right-0 mt-1 w-32 bg-deep-graphite border border-charcoal-outline rounded-lg shadow-lg z-20">
<button
onClick={() => {
onEdit();
setIsOpen(false);
}}
className="w-full px-3 py-2 text-left text-sm text-white hover:bg-iron-gray/50 flex items-center gap-2"
>
<Edit className="w-4 h-4" />
Edit
</button>
<button
onClick={() => {
onVoid();
setIsOpen(false);
}}
className="w-full px-3 py-2 text-left text-sm text-red-400 hover:bg-iron-gray/50 flex items-center gap-2"
>
<Trash2 className="w-4 h-4" />
Void
</button>
</div>
</>
)}
</div>
);
}

View File

@@ -0,0 +1,23 @@
'use client';
import { Plus } from 'lucide-react';
import Button from '../ui/Button';
interface PenaltyFABProps {
onClick: () => void;
}
export default function PenaltyFAB({ onClick }: PenaltyFABProps) {
return (
<div className="fixed bottom-6 right-6 z-50">
<Button
variant="primary"
className="w-14 h-14 rounded-full shadow-lg"
onClick={onClick}
title="Add Penalty"
>
<Plus className="w-6 h-6" />
</Button>
</div>
);
}

View File

@@ -3,14 +3,21 @@
import React, { useState } from 'react';
import { useRouter } from 'next/navigation';
import { getQuickPenaltyUseCase } from '@/lib/di-container';
import type { Driver } from '@gridpilot/racing/application';
import Button from '@/components/ui/Button';
import { AlertTriangle, Clock, Flag, Zap } from 'lucide-react';
interface DriverOption {
id: string;
name: string;
}
interface QuickPenaltyModalProps {
raceId: string;
drivers: Driver[];
raceId?: string;
drivers: DriverOption[];
onClose: () => void;
preSelectedDriver?: DriverOption;
adminId: string;
races?: { id: string; track: string; scheduledAt: Date }[];
}
const INFRACTION_TYPES = [
@@ -28,8 +35,9 @@ const SEVERITY_LEVELS = [
{ value: 'severe', label: 'Severe', description: 'Heavy penalty' },
] as const;
export default function QuickPenaltyModal({ raceId, drivers, onClose }: QuickPenaltyModalProps) {
const [selectedDriver, setSelectedDriver] = useState<string>('');
export default function QuickPenaltyModal({ raceId, drivers, onClose, preSelectedDriver, adminId, races }: QuickPenaltyModalProps) {
const [selectedRaceId, setSelectedRaceId] = useState<string>(raceId || '');
const [selectedDriver, setSelectedDriver] = useState<string>(preSelectedDriver?.id || '');
const [infractionType, setInfractionType] = useState<string>('');
const [severity, setSeverity] = useState<string>('');
const [notes, setNotes] = useState<string>('');
@@ -39,21 +47,24 @@ export default function QuickPenaltyModal({ raceId, drivers, onClose }: QuickPen
const handleSubmit = async (e: React.FormEvent) => {
e.preventDefault();
if (!selectedDriver || !infractionType || !severity) return;
if (!selectedRaceId || !selectedDriver || !infractionType || !severity) return;
setLoading(true);
setError(null);
try {
const useCase = getQuickPenaltyUseCase();
await useCase.execute({
raceId,
const command: any = {
raceId: selectedRaceId,
driverId: selectedDriver,
adminId: 'driver-1', // TODO: Get from current user context
adminId,
infractionType: infractionType as any,
severity: severity as any,
notes: notes.trim() || undefined,
});
};
if (notes.trim()) {
command.notes = notes.trim();
}
await useCase.execute(command);
// Refresh the page to show updated results
router.refresh();
@@ -72,24 +83,52 @@ export default function QuickPenaltyModal({ raceId, drivers, onClose }: QuickPen
<h2 className="text-xl font-bold text-white mb-4">Quick Penalty</h2>
<form onSubmit={handleSubmit} className="space-y-4">
{/* Race Selection */}
{races && !raceId && (
<div>
<label className="block text-sm font-medium text-gray-300 mb-2">
Race
</label>
<select
value={selectedRaceId}
onChange={(e) => setSelectedRaceId(e.target.value)}
className="w-full px-3 py-2 bg-deep-graphite border border-charcoal-outline rounded-lg text-white focus:border-primary-blue focus:outline-none"
required
>
<option value="">Select race...</option>
{races.map((race) => (
<option key={race.id} value={race.id}>
{race.track} ({race.scheduledAt.toLocaleDateString()})
</option>
))}
</select>
</div>
)}
{/* Driver Selection */}
<div>
<label className="block text-sm font-medium text-gray-300 mb-2">
Driver
</label>
<select
value={selectedDriver}
onChange={(e) => setSelectedDriver(e.target.value)}
className="w-full px-3 py-2 bg-deep-graphite border border-charcoal-outline rounded-lg text-white focus:border-primary-blue focus:outline-none"
required
>
<option value="">Select driver...</option>
{drivers.map((driver) => (
<option key={driver.id} value={driver.id}>
{driver.name}
</option>
))}
</select>
{preSelectedDriver ? (
<div className="w-full px-3 py-2 bg-deep-graphite border border-charcoal-outline rounded-lg text-white">
{preSelectedDriver.name}
</div>
) : (
<select
value={selectedDriver}
onChange={(e) => setSelectedDriver(e.target.value)}
className="w-full px-3 py-2 bg-deep-graphite border border-charcoal-outline rounded-lg text-white focus:border-primary-blue focus:outline-none"
required
>
<option value="">Select driver...</option>
{drivers.map((driver) => (
<option key={driver.id} value={driver.id}>
{driver.name}
</option>
))}
</select>
)}
</div>
{/* Infraction Type */}
@@ -175,7 +214,7 @@ export default function QuickPenaltyModal({ raceId, drivers, onClose }: QuickPen
type="submit"
variant="primary"
className="flex-1"
disabled={loading || !selectedDriver || !infractionType || !severity}
disabled={loading || !selectedRaceId || !selectedDriver || !infractionType || !severity}
>
{loading ? 'Applying...' : 'Apply Penalty'}
</Button>

View File

@@ -0,0 +1,41 @@
'use client';
import React from 'react';
import { AlertTriangle } from 'lucide-react';
import Button from '../ui/Button';
interface DriverDTO {
id: string;
name: string;
}
interface InlinePenaltyButtonProps {
driver: DriverDTO;
onPenaltyClick?: (driver: DriverDTO) => void;
isAdmin: boolean;
}
export default function InlinePenaltyButton({
driver,
onPenaltyClick,
isAdmin,
}: InlinePenaltyButtonProps) {
if (!isAdmin) return null;
const handleButtonClick = () => {
if (onPenaltyClick) {
onPenaltyClick(driver);
}
};
return (
<Button
variant="danger"
className="p-1.5 min-h-[32px] w-8 h-8 rounded-full flex items-center justify-center"
onClick={handleButtonClick}
title={`Issue penalty to ${driver.name}`}
>
<AlertTriangle className="w-4 h-4 flex-shrink-0" />
</Button>
);
}

View File

@@ -2,6 +2,7 @@
import Link from 'next/link';
import { AlertTriangle, ExternalLink } from 'lucide-react';
import InlinePenaltyButton from './InlinePenaltyButton';
type PenaltyTypeDTO =
| 'time_penalty'
@@ -41,6 +42,8 @@ interface ResultsTableProps {
fastestLapTime?: number | undefined;
penalties?: PenaltyData[];
currentDriverId?: string | undefined;
isAdmin?: boolean;
onPenaltyClick?: (driver: DriverDTO) => void;
}
export default function ResultsTable({
@@ -50,6 +53,8 @@ export default function ResultsTable({
fastestLapTime,
penalties = [],
currentDriverId,
isAdmin = false,
onPenaltyClick,
}: ResultsTableProps) {
const getDriver = (driverId: string): DriverDTO | undefined => {
return drivers.find((d) => d.id === driverId);
@@ -118,6 +123,7 @@ export default function ResultsTable({
<th className="text-left py-3 px-4 text-sm font-semibold text-gray-400">Points</th>
<th className="text-left py-3 px-4 text-sm font-semibold text-gray-400">+/-</th>
<th className="text-left py-3 px-4 text-sm font-semibold text-gray-400">Penalties</th>
{isAdmin && <th className="text-left py-3 px-4 text-sm font-semibold text-gray-400">Actions</th>}
</tr>
</thead>
<tbody>
@@ -246,6 +252,17 @@ export default function ResultsTable({
<span className="text-gray-500"></span>
)}
</td>
{isAdmin && (
<td className="py-3 px-4">
{driver && onPenaltyClick && (
<InlinePenaltyButton
driver={driver}
onPenaltyClick={onPenaltyClick}
isAdmin={isAdmin}
/>
)}
</td>
)}
</tr>
);
})}