132 lines
4.1 KiB
TypeScript
132 lines
4.1 KiB
TypeScript
'use client';
|
|
|
|
import { useState, useEffect, useCallback } from 'react';
|
|
import { Table } from '@/components/ui/Table';
|
|
import { Button } from '@/components/ui/Button';
|
|
import { listOCRResults, type OCRResultResponse } from '@/lib/ocr';
|
|
import type { PaginatedResponse } from '@/lib/api';
|
|
|
|
interface OCRResultsProps {
|
|
vehicleId?: string;
|
|
onSelectResult?: (result: OCRResultResponse) => void;
|
|
}
|
|
|
|
const STATUS_COLORS: Record<string, string> = {
|
|
pending: 'bg-yellow-100 text-yellow-800',
|
|
processing: 'bg-blue-100 text-blue-800',
|
|
completed: 'bg-green-100 text-green-800',
|
|
failed: 'bg-red-100 text-red-800',
|
|
manual_review: 'bg-orange-100 text-orange-800',
|
|
};
|
|
|
|
export function OCRResults({ vehicleId, onSelectResult }: OCRResultsProps) {
|
|
const [results, setResults] = useState<OCRResultResponse[]>([]);
|
|
const [total, setTotal] = useState(0);
|
|
const [page, setPage] = useState(1);
|
|
const [pageSize] = useState(20);
|
|
const [loading, setLoading] = useState(false);
|
|
const [error, setError] = useState<string | null>(null);
|
|
|
|
const fetchResults = useCallback(async () => {
|
|
setLoading(true);
|
|
setError(null);
|
|
try {
|
|
const data: PaginatedResponse<OCRResultResponse> = await listOCRResults(vehicleId, page, pageSize);
|
|
setResults(data.items);
|
|
setTotal(data.total);
|
|
} catch (err: unknown) {
|
|
const apiErr = err as { error?: { message?: string } };
|
|
setError(apiErr?.error?.message || 'Failed to load OCR results');
|
|
} finally {
|
|
setLoading(false);
|
|
}
|
|
}, [vehicleId, page, pageSize]);
|
|
|
|
useEffect(() => {
|
|
fetchResults();
|
|
}, [fetchResults]);
|
|
|
|
const columns = [
|
|
{
|
|
key: 'file_name',
|
|
label: 'File',
|
|
render: (row: OCRResultResponse) => (
|
|
<button
|
|
data-testid={`ocr-row-${row.id}`}
|
|
onClick={() => onSelectResult?.(row)}
|
|
className="text-primary hover:underline"
|
|
>
|
|
{row.file_name}
|
|
</button>
|
|
),
|
|
},
|
|
{ key: 'status', label: 'Status', render: (row: OCRResultResponse) => (
|
|
<span
|
|
data-testid={`ocr-status-${row.id}`}
|
|
className={`px-2 py-1 rounded text-xs font-medium ${STATUS_COLORS[row.status] || 'bg-gray-100 text-gray-800'}`}
|
|
>
|
|
{row.status}
|
|
</span>
|
|
)},
|
|
{ key: 'confidence_score', label: 'Confidence', render: (row: OCRResultResponse) =>
|
|
row.confidence_score != null
|
|
? `${(row.confidence_score * 100).toFixed(1)}%`
|
|
: '-'
|
|
},
|
|
{ key: 'created_at', label: 'Created', render: (row: OCRResultResponse) =>
|
|
row.created_at ? new Date(row.created_at).toLocaleDateString('de-DE') : '-'
|
|
},
|
|
];
|
|
|
|
const totalPages = Math.ceil(total / pageSize);
|
|
|
|
return (
|
|
<div data-testid="ocr-results" className="space-y-4">
|
|
<div className="flex items-center justify-between">
|
|
<h2 className="text-xl font-bold text-text">OCR Results</h2>
|
|
<Button variant="secondary" onClick={fetchResults} loading={loading}>
|
|
Refresh
|
|
</Button>
|
|
</div>
|
|
|
|
{error && (
|
|
<div data-testid="ocr-results-error" className="p-4 bg-error/10 text-error rounded-lg">
|
|
{error}
|
|
</div>
|
|
)}
|
|
|
|
{loading ? (
|
|
<div data-testid="ocr-results-loading" className="text-center py-8 text-text-muted">
|
|
Loading OCR results...
|
|
</div>
|
|
) : (
|
|
<Table columns={columns} data={results} rowKey={row => row.id} />
|
|
)}
|
|
|
|
{totalPages > 1 && (
|
|
<div data-testid="ocr-results-pagination" className="flex items-center justify-between">
|
|
<span className="text-sm text-text-muted">
|
|
Page {page} of {totalPages} ({total} total)
|
|
</span>
|
|
<div className="flex gap-2">
|
|
<Button
|
|
variant="secondary"
|
|
disabled={page <= 1}
|
|
onClick={() => setPage(page - 1)}
|
|
>
|
|
Previous
|
|
</Button>
|
|
<Button
|
|
variant="secondary"
|
|
disabled={page >= totalPages}
|
|
onClick={() => setPage(page + 1)}
|
|
>
|
|
Next
|
|
</Button>
|
|
</div>
|
|
</div>
|
|
)}
|
|
</div>
|
|
);
|
|
}
|