132 lines
5.5 KiB
TypeScript
132 lines
5.5 KiB
TypeScript
|
|
'use client';
|
||
|
|
|
||
|
|
import { useState } from 'react';
|
||
|
|
import { Button } from '@/components/ui/Button';
|
||
|
|
import { Card } from '@/components/ui/Card';
|
||
|
|
import { applyOCRToVehicle, type OCRResultResponse } from '@/lib/ocr';
|
||
|
|
|
||
|
|
interface OCRDetailProps {
|
||
|
|
result: OCRResultResponse;
|
||
|
|
onApplyComplete?: () => void;
|
||
|
|
}
|
||
|
|
|
||
|
|
export function OCRDetail({ result, onApplyComplete }: OCRDetailProps) {
|
||
|
|
const [applying, setApplying] = useState(false);
|
||
|
|
const [applyError, setApplyError] = useState<string | null>(null);
|
||
|
|
const [applySuccess, setApplySuccess] = useState<string | null>(null);
|
||
|
|
|
||
|
|
const handleApply = async () => {
|
||
|
|
setApplying(true);
|
||
|
|
setApplyError(null);
|
||
|
|
setApplySuccess(null);
|
||
|
|
try {
|
||
|
|
const response = await applyOCRToVehicle(result.id);
|
||
|
|
setApplySuccess(`Applied ${response.updated_fields.length} fields to vehicle`);
|
||
|
|
if (onApplyComplete) onApplyComplete();
|
||
|
|
} catch (err: unknown) {
|
||
|
|
const apiErr = err as { error?: { message?: string } };
|
||
|
|
setApplyError(apiErr?.error?.message || 'Failed to apply OCR data');
|
||
|
|
} finally {
|
||
|
|
setApplying(false);
|
||
|
|
}
|
||
|
|
};
|
||
|
|
|
||
|
|
const data = result.structured_data;
|
||
|
|
const canApply = result.vehicle_id && result.structured_data && result.status === 'completed';
|
||
|
|
|
||
|
|
return (
|
||
|
|
<div data-testid="ocr-detail" className="space-y-4">
|
||
|
|
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
|
||
|
|
{/* Original Scan Side */}
|
||
|
|
<Card title="Original Scan">
|
||
|
|
<div data-testid="ocr-original-scan" className="space-y-2">
|
||
|
|
<p className="text-sm text-text-muted">File: {result.file_name}</p>
|
||
|
|
<p className="text-sm text-text-muted">Type: {result.mime_type}</p>
|
||
|
|
<p className="text-sm text-text-muted">Status: {result.status}</p>
|
||
|
|
{result.confidence_score != null && (
|
||
|
|
<p className="text-sm text-text-muted">
|
||
|
|
Confidence: {(result.confidence_score * 100).toFixed(1)}%
|
||
|
|
</p>
|
||
|
|
)}
|
||
|
|
{result.error_message && (
|
||
|
|
<p className="text-sm text-error">Error: {result.error_message}</p>
|
||
|
|
)}
|
||
|
|
{result.raw_text && (
|
||
|
|
<div className="mt-4">
|
||
|
|
<p className="text-sm font-medium text-text mb-1">Raw Text:</p>
|
||
|
|
<pre data-testid="ocr-raw-text" className="text-xs bg-gray-50 p-3 rounded overflow-auto max-h-48">
|
||
|
|
{result.raw_text}
|
||
|
|
</pre>
|
||
|
|
</div>
|
||
|
|
)}
|
||
|
|
</div>
|
||
|
|
</Card>
|
||
|
|
|
||
|
|
{/* Extracted Data Side */}
|
||
|
|
<Card title="Extracted Data">
|
||
|
|
<div data-testid="ocr-extracted-data" className="space-y-2">
|
||
|
|
{data ? (
|
||
|
|
<dl className="space-y-2">
|
||
|
|
<div className="flex justify-between">
|
||
|
|
<dt className="text-sm font-medium text-text">Brand:</dt>
|
||
|
|
<dd className="text-sm text-text-muted" data-testid="ocr-field-brand">{data.brand || '-'}</dd>
|
||
|
|
</div>
|
||
|
|
<div className="flex justify-between">
|
||
|
|
<dt className="text-sm font-medium text-text">Model:</dt>
|
||
|
|
<dd className="text-sm text-text-muted" data-testid="ocr-field-model">{data.model || '-'}</dd>
|
||
|
|
</div>
|
||
|
|
<div className="flex justify-between">
|
||
|
|
<dt className="text-sm font-medium text-text">VIN:</dt>
|
||
|
|
<dd className="text-sm text-text-muted" data-testid="ocr-field-vin">{data.vin || '-'}</dd>
|
||
|
|
</div>
|
||
|
|
<div className="flex justify-between">
|
||
|
|
<dt className="text-sm font-medium text-text">First Registration:</dt>
|
||
|
|
<dd className="text-sm text-text-muted" data-testid="ocr-field-first_registration">{data.first_registration || '-'}</dd>
|
||
|
|
</div>
|
||
|
|
<div className="flex justify-between">
|
||
|
|
<dt className="text-sm font-medium text-text">Mileage:</dt>
|
||
|
|
<dd className="text-sm text-text-muted" data-testid="ocr-field-mileage">{data.mileage != null ? `${data.mileage} km` : '-'}</dd>
|
||
|
|
</div>
|
||
|
|
<div className="flex justify-between">
|
||
|
|
<dt className="text-sm font-medium text-text">Power (kW):</dt>
|
||
|
|
<dd className="text-sm text-text-muted" data-testid="ocr-field-power_kw">{data.power_kw != null ? `${data.power_kw} kW` : '-'}</dd>
|
||
|
|
</div>
|
||
|
|
<div className="flex justify-between">
|
||
|
|
<dt className="text-sm font-medium text-text">Fuel Type:</dt>
|
||
|
|
<dd className="text-sm text-text-muted" data-testid="ocr-field-fuel_type">{data.fuel_type || '-'}</dd>
|
||
|
|
</div>
|
||
|
|
</dl>
|
||
|
|
) : (
|
||
|
|
<p className="text-sm text-text-muted">No structured data available yet.</p>
|
||
|
|
)}
|
||
|
|
|
||
|
|
{canApply && (
|
||
|
|
<div className="mt-4">
|
||
|
|
<Button
|
||
|
|
data-testid="ocr-apply-button"
|
||
|
|
onClick={handleApply}
|
||
|
|
loading={applying}
|
||
|
|
>
|
||
|
|
Apply to Vehicle
|
||
|
|
</Button>
|
||
|
|
</div>
|
||
|
|
)}
|
||
|
|
|
||
|
|
{applyError && (
|
||
|
|
<div data-testid="ocr-apply-error" className="mt-2 p-3 bg-error/10 text-error rounded text-sm">
|
||
|
|
{applyError}
|
||
|
|
</div>
|
||
|
|
)}
|
||
|
|
|
||
|
|
{applySuccess && (
|
||
|
|
<div data-testid="ocr-apply-success" className="mt-2 p-3 bg-green-50 text-green-700 rounded text-sm">
|
||
|
|
{applySuccess}
|
||
|
|
</div>
|
||
|
|
)}
|
||
|
|
</div>
|
||
|
|
</Card>
|
||
|
|
</div>
|
||
|
|
</div>
|
||
|
|
);
|
||
|
|
}
|