122 lines
3.8 KiB
TypeScript
122 lines
3.8 KiB
TypeScript
'use client';
|
||
|
||
import { useEffect, useState } from 'react';
|
||
import Progress from './Progress';
|
||
import { Recognition } from '../../types/plates';
|
||
import Checkbox from './Checkbox';
|
||
|
||
type Props = {
|
||
entry: Recognition;
|
||
isSelected?: boolean;
|
||
isNew?: boolean;
|
||
onClick?: () => void;
|
||
|
||
// neu für Checkbox:
|
||
checked?: boolean;
|
||
onToggle?: () => void;
|
||
};
|
||
|
||
export default function RecognitionRow({
|
||
entry,
|
||
isSelected = false,
|
||
isNew = false,
|
||
onClick,
|
||
checked = false,
|
||
onToggle,
|
||
}: Props) {
|
||
const [animatedConfidence, setAnimatedConfidence] = useState(0);
|
||
|
||
useEffect(() => {
|
||
if (isNew) {
|
||
setAnimatedConfidence(0);
|
||
setTimeout(() => setAnimatedConfidence(entry.confidence ?? 0), 50);
|
||
} else {
|
||
setAnimatedConfidence(entry.confidence ?? 0);
|
||
}
|
||
}, [entry.confidence, isNew]);
|
||
|
||
const rowClass = [
|
||
'cursor-pointer',
|
||
isSelected && !isNew ? 'bg-gray-200 dark:bg-neutral-700' : '',
|
||
!isSelected ? 'hover:bg-gray-100 dark:hover:bg-neutral-600' : '',
|
||
isNew ? 'bg-green-50 dark:bg-green-600' : ''
|
||
].filter(Boolean).join(' ');
|
||
|
||
const showDirIcon =
|
||
typeof entry.directionDegrees === 'number' && entry.directionDegrees > 0;
|
||
|
||
const dirText = entry.direction
|
||
? entry.direction.toLowerCase() === 'away'
|
||
? 'abfahrend'
|
||
: entry.direction.toLowerCase() === 'towards'
|
||
? 'ankommend'
|
||
: '–'
|
||
: '–';
|
||
|
||
return (
|
||
<tr onClick={onClick} className={rowClass}>
|
||
{/* 1) Checkbox-Spalte */}
|
||
<td
|
||
className="px-6 py-4 whitespace-nowrap text-sm text-center"
|
||
onClick={(e) => e.stopPropagation()}
|
||
>
|
||
<Checkbox
|
||
id={`row-${entry.id}`}
|
||
checked={checked}
|
||
// indeterminate brauchst du auf Zeilenebene nicht → weglassen oder false
|
||
onChange={() => onToggle?.()}
|
||
label={
|
||
<span className="sr-only">
|
||
Zeile für {entry.licenseFormatted ?? entry.license} auswählen
|
||
</span>
|
||
}
|
||
containerClassName="justify-center"
|
||
/>
|
||
</td>
|
||
|
||
{/* 2) restliche Spalten – Anzahl muss zum Head passen */}
|
||
<td className="px-6 py-4 whitespace-nowrap text-sm font-medium text-gray-800 dark:text-neutral-200">
|
||
{entry.licenseFormatted ?? entry.license}
|
||
</td>
|
||
<td className="px-6 py-4 whitespace-nowrap text-sm text-gray-600 dark:text-neutral-300">
|
||
{entry.country ?? '–'}
|
||
</td>
|
||
<td className="px-6 py-4 whitespace-nowrap text-sm text-gray-600 dark:text-neutral-300">
|
||
{entry.brand ?? '–'}
|
||
</td>
|
||
<td className="px-6 py-4 whitespace-nowrap text-sm text-gray-600 dark:text-neutral-300">
|
||
{entry.model ?? '–'}
|
||
</td>
|
||
<td className="px-6 py-4 whitespace-nowrap text-sm text-gray-600 dark:text-neutral-300">
|
||
<Progress value={animatedConfidence} />
|
||
</td>
|
||
{/* Richtung hat im Head colSpan={2} → hier 2 Zellen */}
|
||
<td className="px-6 py-4 whitespace-nowrap text-sm text-gray-600 dark:text-neutral-300">
|
||
{showDirIcon && (
|
||
<svg
|
||
className="w-4 h-4"
|
||
viewBox="0 0 24 24"
|
||
fill="none"
|
||
stroke="currentColor"
|
||
strokeWidth={2}
|
||
strokeLinecap="round"
|
||
strokeLinejoin="round"
|
||
style={{ transform: `rotate(${entry.directionDegrees}deg)`, transformOrigin: 'center' }}
|
||
>
|
||
<path d="M12 2v20M5 9l7-7 7 7" />
|
||
</svg>
|
||
)}
|
||
</td>
|
||
<td className="px-6 py-4 whitespace-nowrap text-sm text-gray-600 dark:text-neutral-300">
|
||
{dirText}
|
||
</td>
|
||
<td className="px-6 py-4 whitespace-nowrap text-sm text-gray-600 dark:text-neutral-300">
|
||
{new Date(entry.timestampLocal).toLocaleString('de-DE')}
|
||
</td>
|
||
<td className="px-6 py-4 whitespace-nowrap text-sm text-gray-600 dark:text-neutral-300">
|
||
{entry.cameraName ?? '–'}
|
||
</td>
|
||
</tr>
|
||
);
|
||
}
|