feat: Improve VIN decode confidence reporting and make/model/trim editability (#125) #126

Merged
egullickson merged 1 commits from issue-125-improve-vin-confidence-editability into main 2026-02-09 01:40:15 +00:00
3 changed files with 486 additions and 229 deletions
Showing only changes of commit e9020dbb2f - Show all commits

View File

@@ -7,7 +7,7 @@ import { useForm } from 'react-hook-form';
import { zodResolver } from '@hookform/resolvers/zod';
import { z } from 'zod';
import { Button } from '../../../shared-minimal/components/Button';
import { CreateVehicleRequest, Vehicle } from '../types/vehicles.types';
import { CreateVehicleRequest, Vehicle, VinReviewSelections } from '../types/vehicles.types';
import { vehiclesApi } from '../api/vehicles.api';
import { VehicleImageUpload } from './VehicleImageUpload';
import { useTierAccess } from '../../../core/hooks/useTierAccess';
@@ -433,52 +433,47 @@ export const VehicleForm: React.FC<VehicleFormProps> = ({
const watchedVin = watch('vin');
/**
* Handle accepting VIN OCR result
* Populates VIN and decoded fields into the form
* Handle accepting VIN OCR result with user-edited selections from review modal
* Populates VIN and selected dropdown values into the form
*/
const handleAcceptVinOcr = async () => {
const result = vinOcr.acceptResult();
if (!result) return;
const { ocrResult, decodedVehicle } = result;
const handleAcceptVinOcr = async (selections: VinReviewSelections) => {
// Clear the OCR result state
vinOcr.acceptResult();
// Set the VIN immediately
setValue('vin', ocrResult.vin);
setValue('vin', selections.vin);
// If we have decoded vehicle data, populate the form similar to handleDecodeVin
if (decodedVehicle) {
// Populate form with user's dropdown selections
const hasSelections = selections.year || selections.make || selections.model ||
selections.trimLevel || selections.engine || selections.transmission;
if (hasSelections) {
// Prevent cascade useEffects from clearing values
isVinDecoding.current = true;
setLoadingDropdowns(true);
try {
// Determine final values
const yearValue = decodedVehicle.year.value;
const makeValue = decodedVehicle.make.value;
const modelValue = decodedVehicle.model.value;
const trimValue = decodedVehicle.trimLevel.value;
// Load dropdown options hierarchically
if (yearValue) {
prevYear.current = yearValue;
const makesData = await vehiclesApi.getMakes(yearValue);
// Load dropdown options hierarchically for the selected values
if (selections.year) {
prevYear.current = selections.year;
const makesData = await vehiclesApi.getMakes(selections.year);
setMakes(makesData);
if (makeValue) {
prevMake.current = makeValue;
const modelsData = await vehiclesApi.getModels(yearValue, makeValue);
if (selections.make) {
prevMake.current = selections.make;
const modelsData = await vehiclesApi.getModels(selections.year, selections.make);
setModels(modelsData);
if (modelValue) {
prevModel.current = modelValue;
const trimsData = await vehiclesApi.getTrims(yearValue, makeValue, modelValue);
if (selections.model) {
prevModel.current = selections.model;
const trimsData = await vehiclesApi.getTrims(selections.year, selections.make, selections.model);
setTrims(trimsData);
if (trimValue) {
prevTrim.current = trimValue;
if (selections.trimLevel) {
prevTrim.current = selections.trimLevel;
const [enginesData, transmissionsData] = await Promise.all([
vehiclesApi.getEngines(yearValue, makeValue, modelValue, trimValue),
vehiclesApi.getTransmissions(yearValue, makeValue, modelValue, trimValue),
vehiclesApi.getEngines(selections.year, selections.make, selections.model, selections.trimLevel),
vehiclesApi.getTransmissions(selections.year, selections.make, selections.model, selections.trimLevel),
]);
setEngines(enginesData);
setTransmissions(transmissionsData);
@@ -488,24 +483,12 @@ export const VehicleForm: React.FC<VehicleFormProps> = ({
}
// Set form values after options are loaded
if (decodedVehicle.year.value) {
setValue('year', decodedVehicle.year.value);
}
if (decodedVehicle.make.value) {
setValue('make', decodedVehicle.make.value);
}
if (decodedVehicle.model.value) {
setValue('model', decodedVehicle.model.value);
}
if (decodedVehicle.trimLevel.value) {
setValue('trimLevel', decodedVehicle.trimLevel.value);
}
if (decodedVehicle.engine.value) {
setValue('engine', decodedVehicle.engine.value);
}
if (decodedVehicle.transmission.value) {
setValue('transmission', decodedVehicle.transmission.value);
}
if (selections.year) setValue('year', selections.year);
if (selections.make) setValue('make', selections.make);
if (selections.model) setValue('model', selections.model);
if (selections.trimLevel) setValue('trimLevel', selections.trimLevel);
if (selections.engine) setValue('engine', selections.engine);
if (selections.transmission) setValue('transmission', selections.transmission);
} finally {
setLoadingDropdowns(false);
isVinDecoding.current = false;
@@ -513,17 +496,6 @@ export const VehicleForm: React.FC<VehicleFormProps> = ({
}
};
/**
* Handle editing manually after VIN OCR
* Just sets the VIN and closes the modal
*/
const handleEditVinManually = () => {
const result = vinOcr.acceptResult();
if (result) {
setValue('vin', result.ocrResult.vin);
}
};
/**
* Handle retaking VIN photo
* Resets and restarts capture
@@ -1007,7 +979,6 @@ export const VehicleForm: React.FC<VehicleFormProps> = ({
open={!!vinOcr.result}
result={vinOcr.result}
onAccept={handleAcceptVinOcr}
onEdit={handleEditVinManually}
onRetake={handleRetakeVinPhoto}
onClose={vinOcr.reset}
/>

View File

@@ -1,9 +1,9 @@
/**
* @ai-summary Modal to review VIN OCR results and decoded vehicle data
* @ai-context Shows extracted VIN with confidence, decoded fields, accept/edit/retake options
* @ai-summary Modal to review VIN OCR results with editable cascade dropdowns
* @ai-context Shows extracted VIN with OCR confidence, editable vehicle dropdowns, accept/retake options
*/
import React from 'react';
import React, { useState, useEffect } from 'react';
import {
Dialog,
DialogTitle,
@@ -16,129 +16,290 @@ import {
useTheme,
useMediaQuery,
Drawer,
Divider,
LinearProgress,
} from '@mui/material';
import CheckCircleIcon from '@mui/icons-material/CheckCircle';
import WarningIcon from '@mui/icons-material/Warning';
import ErrorIcon from '@mui/icons-material/Error';
import CameraAltIcon from '@mui/icons-material/CameraAlt';
import EditIcon from '@mui/icons-material/Edit';
import { VinCaptureResult } from '../hooks/useVinOcr';
import { MatchConfidence } from '../types/vehicles.types';
import { VinReviewSelections } from '../types/vehicles.types';
import { vehiclesApi } from '../api/vehicles.api';
interface VinOcrReviewModalProps {
open: boolean;
result: VinCaptureResult | null;
onAccept: () => void;
onEdit: () => void;
onAccept: (selections: VinReviewSelections) => void;
onRetake: () => void;
onClose: () => void;
}
/** Get confidence level from percentage */
/** Get confidence level from OCR percentage */
function getConfidenceLevel(confidence: number): 'high' | 'medium' | 'low' {
if (confidence >= 0.9) return 'high';
if (confidence >= 0.7) return 'medium';
return 'low';
}
/** Confidence indicator component */
const ConfidenceIndicator: React.FC<{ level: 'high' | 'medium' | 'low' | 'none' }> = ({
/** VIN OCR confidence indicator with percentage */
const ConfidenceIndicator: React.FC<{ level: 'high' | 'medium' | 'low'; percentage: number }> = ({
level,
percentage,
}) => {
const configs = {
high: { color: 'success.main', icon: CheckCircleIcon, label: 'High' },
medium: { color: 'warning.main', icon: WarningIcon, label: 'Medium' },
low: { color: 'error.light', icon: ErrorIcon, label: 'Low' },
none: { color: 'text.disabled', icon: ErrorIcon, label: 'N/A' },
};
const config = configs[level];
const Icon = config.icon;
return (
<Box
sx={{
display: 'flex',
alignItems: 'center',
gap: 0.5,
}}
>
<Box sx={{ display: 'flex', alignItems: 'center', gap: 0.5 }}>
<Icon sx={{ fontSize: 16, color: config.color }} />
<Typography variant="caption" sx={{ color: config.color }}>
{config.label}
{config.label} ({Math.round(percentage * 100)}%)
</Typography>
</Box>
);
};
/** Map match confidence to display level */
function matchConfidenceToLevel(confidence: MatchConfidence): 'high' | 'medium' | 'low' | 'none' {
switch (confidence) {
case 'high':
return 'high';
case 'medium':
return 'medium';
case 'none':
return 'none';
default:
return 'none';
}
}
/** Shared select classes matching VehicleForm styling */
const selectClasses =
'w-full px-3 py-2 border rounded-md min-h-[44px] bg-white text-gray-900 border-gray-300 focus:outline-none focus:ring-2 focus:ring-primary-500 dark:bg-scuro dark:text-avus dark:border-silverstone dark:focus:ring-abudhabi dark:focus:border-abudhabi';
/** Decoded field row component */
const DecodedFieldRow: React.FC<{
label: string;
value: string | number | null;
nhtsaValue: string | null;
confidence: MatchConfidence;
}> = ({ label, value, nhtsaValue, confidence }) => {
const displayValue = value || nhtsaValue || '-';
const level = matchConfidenceToLevel(confidence);
return (
<Box
sx={{
display: 'flex',
justifyContent: 'space-between',
alignItems: 'center',
py: 1,
px: 2,
backgroundColor: level === 'low' || level === 'none' ? 'action.hover' : 'transparent',
borderRadius: 1,
}}
>
<Box>
<Typography variant="body2" color="text.secondary">
{label}
</Typography>
<Typography variant="body1" fontWeight={value ? 500 : 400}>
{displayValue}
</Typography>
{nhtsaValue && value !== nhtsaValue && (
<Typography variant="caption" color="text.secondary">
NHTSA: {nhtsaValue}
</Typography>
)}
</Box>
<ConfidenceIndicator level={level} />
</Box>
);
};
/** Main modal content */
/** Main modal content with cascade dropdowns */
const ReviewContent: React.FC<{
result: VinCaptureResult;
onAccept: () => void;
onEdit: () => void;
onAccept: (selections: VinReviewSelections) => void;
onRetake: () => void;
}> = ({ result, onAccept, onEdit, onRetake }) => {
}> = ({ result, onAccept, onRetake }) => {
const { ocrResult, decodedVehicle, decodeError } = result;
const vinConfidenceLevel = getConfidenceLevel(ocrResult.confidence);
// Dropdown option arrays
const [years, setYears] = useState<number[]>([]);
const [makes, setMakes] = useState<string[]>([]);
const [models, setModels] = useState<string[]>([]);
const [trims, setTrims] = useState<string[]>([]);
const [engines, setEngines] = useState<string[]>([]);
const [transmissions, setTransmissions] = useState<string[]>([]);
const [loadingDropdowns, setLoadingDropdowns] = useState(false);
// Selected values
const [selectedYear, setSelectedYear] = useState<number | undefined>(undefined);
const [selectedMake, setSelectedMake] = useState('');
const [selectedModel, setSelectedModel] = useState('');
const [selectedTrim, setSelectedTrim] = useState('');
const [selectedEngine, setSelectedEngine] = useState('');
const [selectedTransmission, setSelectedTransmission] = useState('');
// NHTSA reference values for unmatched fields
const [nhtsaRefs, setNhtsaRefs] = useState<Record<string, string | null>>({});
// Initialize dropdown options and pre-select decoded values
useEffect(() => {
const initialize = async () => {
setLoadingDropdowns(true);
try {
const yearsData = await vehiclesApi.getYears();
setYears(yearsData);
if (!decodedVehicle) return;
// Store NHTSA reference values for unmatched fields
setNhtsaRefs({
make: decodedVehicle.make.confidence === 'none' ? decodedVehicle.make.nhtsaValue : null,
model: decodedVehicle.model.confidence === 'none' ? decodedVehicle.model.nhtsaValue : null,
trim: decodedVehicle.trimLevel.confidence === 'none' ? decodedVehicle.trimLevel.nhtsaValue : null,
engine: decodedVehicle.engine.confidence === 'none' ? decodedVehicle.engine.nhtsaValue : null,
transmission: decodedVehicle.transmission.confidence === 'none' ? decodedVehicle.transmission.nhtsaValue : null,
});
const yearValue = decodedVehicle.year.value;
if (!yearValue) return;
setSelectedYear(yearValue);
const makesData = await vehiclesApi.getMakes(yearValue);
setMakes(makesData);
const makeValue = decodedVehicle.make.value;
if (!makeValue) return;
setSelectedMake(makeValue);
const modelsData = await vehiclesApi.getModels(yearValue, makeValue);
setModels(modelsData);
const modelValue = decodedVehicle.model.value;
if (!modelValue) return;
setSelectedModel(modelValue);
const trimsData = await vehiclesApi.getTrims(yearValue, makeValue, modelValue);
setTrims(trimsData);
const trimValue = decodedVehicle.trimLevel.value;
if (!trimValue) return;
setSelectedTrim(trimValue);
const [enginesData, transmissionsData] = await Promise.all([
vehiclesApi.getEngines(yearValue, makeValue, modelValue, trimValue),
vehiclesApi.getTransmissions(yearValue, makeValue, modelValue, trimValue),
]);
setEngines(enginesData);
setTransmissions(transmissionsData);
if (decodedVehicle.engine.value) setSelectedEngine(decodedVehicle.engine.value);
if (decodedVehicle.transmission.value) setSelectedTransmission(decodedVehicle.transmission.value);
} catch (error) {
console.error('Failed to initialize review modal dropdowns:', error);
} finally {
setLoadingDropdowns(false);
}
};
initialize();
// eslint-disable-next-line react-hooks/exhaustive-deps
}, []);
// Cascade handlers
const handleYearChange = async (year: number | undefined) => {
setSelectedYear(year);
setSelectedMake('');
setSelectedModel('');
setSelectedTrim('');
setSelectedEngine('');
setSelectedTransmission('');
setModels([]);
setTrims([]);
setEngines([]);
setTransmissions([]);
if (year) {
setLoadingDropdowns(true);
try {
const makesData = await vehiclesApi.getMakes(year);
setMakes(makesData);
} catch {
setMakes([]);
} finally {
setLoadingDropdowns(false);
}
} else {
setMakes([]);
}
};
const handleMakeChange = async (make: string) => {
setSelectedMake(make);
setSelectedModel('');
setSelectedTrim('');
setSelectedEngine('');
setSelectedTransmission('');
setTrims([]);
setEngines([]);
setTransmissions([]);
if (make && selectedYear) {
setLoadingDropdowns(true);
try {
const modelsData = await vehiclesApi.getModels(selectedYear, make);
setModels(modelsData);
} catch {
setModels([]);
} finally {
setLoadingDropdowns(false);
}
} else {
setModels([]);
}
};
const handleModelChange = async (model: string) => {
setSelectedModel(model);
setSelectedTrim('');
setSelectedEngine('');
setSelectedTransmission('');
setEngines([]);
setTransmissions([]);
if (model && selectedYear && selectedMake) {
setLoadingDropdowns(true);
try {
const trimsData = await vehiclesApi.getTrims(selectedYear, selectedMake, model);
setTrims(trimsData);
} catch {
setTrims([]);
} finally {
setLoadingDropdowns(false);
}
} else {
setTrims([]);
}
};
const handleTrimChange = async (trim: string) => {
setSelectedTrim(trim);
setSelectedEngine('');
setSelectedTransmission('');
if (trim && selectedYear && selectedMake && selectedModel) {
setLoadingDropdowns(true);
try {
const [enginesData, transmissionsData] = await Promise.all([
vehiclesApi.getEngines(selectedYear, selectedMake, selectedModel, trim),
vehiclesApi.getTransmissions(selectedYear, selectedMake, selectedModel, trim),
]);
setEngines(enginesData);
setTransmissions(transmissionsData);
} catch {
setEngines([]);
setTransmissions([]);
} finally {
setLoadingDropdowns(false);
}
} else {
setEngines([]);
setTransmissions([]);
}
};
const handleAccept = () => {
onAccept({
vin: ocrResult.vin,
year: selectedYear,
make: selectedMake || undefined,
model: selectedModel || undefined,
trimLevel: selectedTrim || undefined,
engine: selectedEngine || undefined,
transmission: selectedTransmission || undefined,
});
};
/** Show NHTSA reference when field had no dropdown match */
const nhtsaHint = (field: string) => {
const ref = nhtsaRefs[field];
if (!ref) return null;
// Only show hint when no value is currently selected
const selected: Record<string, string> = {
make: selectedMake,
model: selectedModel,
trim: selectedTrim,
engine: selectedEngine,
transmission: selectedTransmission,
};
if (selected[field]) return null;
return (
<p className="mt-1 text-xs text-gray-500 dark:text-titanio">
NHTSA returned: {ref}
</p>
);
};
return (
<>
{/* VIN Section */}
{/* VIN Section - OCR confidence only */}
<Box sx={{ mb: 3 }}>
<Typography variant="subtitle2" color="text.secondary" gutterBottom>
Detected VIN
@@ -152,7 +313,12 @@ const ReviewContent: React.FC<{
backgroundColor: 'action.hover',
borderRadius: 1,
border: 1,
borderColor: vinConfidenceLevel === 'high' ? 'success.main' : 'warning.main',
borderColor:
vinConfidenceLevel === 'high'
? 'success.main'
: vinConfidenceLevel === 'medium'
? 'warning.main'
: 'error.light',
}}
>
<Typography
@@ -163,11 +329,13 @@ const ReviewContent: React.FC<{
>
{ocrResult.vin}
</Typography>
<ConfidenceIndicator level={vinConfidenceLevel} />
<ConfidenceIndicator level={vinConfidenceLevel} percentage={ocrResult.confidence} />
</Box>
{vinConfidenceLevel !== 'high' && (
<Typography variant="caption" color="warning.main" sx={{ mt: 1, display: 'block' }}>
Low confidence detection - please verify the VIN is correct
{vinConfidenceLevel === 'low'
? 'Low confidence detection - please verify the VIN is correct'
: 'Medium confidence - please verify the VIN is correct'}
</Typography>
)}
</Box>
@@ -179,73 +347,197 @@ const ReviewContent: React.FC<{
</Alert>
)}
{/* Decoded Vehicle Information */}
{decodedVehicle && (
<Box>
<Typography variant="subtitle2" color="text.secondary" gutterBottom>
Decoded Vehicle Information
</Typography>
<Box
sx={{
border: 1,
borderColor: 'divider',
borderRadius: 1,
overflow: 'hidden',
}}
>
<DecodedFieldRow
label="Year"
value={decodedVehicle.year.value}
nhtsaValue={decodedVehicle.year.nhtsaValue}
confidence={decodedVehicle.year.confidence}
/>
<Divider />
<DecodedFieldRow
label="Make"
value={decodedVehicle.make.value}
nhtsaValue={decodedVehicle.make.nhtsaValue}
confidence={decodedVehicle.make.confidence}
/>
<Divider />
<DecodedFieldRow
label="Model"
value={decodedVehicle.model.value}
nhtsaValue={decodedVehicle.model.nhtsaValue}
confidence={decodedVehicle.model.confidence}
/>
<Divider />
<DecodedFieldRow
label="Trim"
value={decodedVehicle.trimLevel.value}
nhtsaValue={decodedVehicle.trimLevel.nhtsaValue}
confidence={decodedVehicle.trimLevel.confidence}
/>
<Divider />
<DecodedFieldRow
label="Engine"
value={decodedVehicle.engine.value}
nhtsaValue={decodedVehicle.engine.nhtsaValue}
confidence={decodedVehicle.engine.confidence}
/>
<Divider />
<DecodedFieldRow
label="Transmission"
value={decodedVehicle.transmission.value}
nhtsaValue={decodedVehicle.transmission.nhtsaValue}
confidence={decodedVehicle.transmission.confidence}
/>
</Box>
{/* Loading indicator */}
{loadingDropdowns && <LinearProgress sx={{ mb: 2 }} />}
<Typography variant="caption" color="text.secondary" sx={{ mt: 2, display: 'block' }}>
Fields with lower confidence may need manual verification.
</Typography>
</Box>
)}
{/* Vehicle Details - Editable Cascade Dropdowns */}
<Box sx={{ mb: 2 }}>
<Typography variant="subtitle2" color="text.secondary" gutterBottom>
Vehicle Details
</Typography>
<Typography variant="caption" color="text.secondary" sx={{ display: 'block', mb: 2 }}>
Review and adjust the vehicle details below.
</Typography>
<div className="grid grid-cols-1 sm:grid-cols-2 gap-3">
{/* Year */}
<div>
<label className="block text-sm font-medium text-gray-700 dark:text-avus mb-1">
Year
</label>
<select
value={selectedYear || ''}
onChange={(e) =>
handleYearChange(e.target.value ? parseInt(e.target.value) : undefined)
}
className={selectClasses}
style={{ fontSize: '16px' }}
>
<option value="">Select Year</option>
{years.map((year) => (
<option key={year} value={year}>
{year}
</option>
))}
</select>
</div>
{/* Make */}
<div>
<label className="block text-sm font-medium text-gray-700 dark:text-avus mb-1">
Make
</label>
<select
value={selectedMake}
onChange={(e) => handleMakeChange(e.target.value)}
className={selectClasses}
style={{ fontSize: '16px' }}
disabled={loadingDropdowns || !selectedYear || makes.length === 0}
>
<option value="">
{loadingDropdowns
? 'Loading...'
: !selectedYear
? 'Select year first'
: makes.length === 0
? 'No makes available'
: 'Select Make'}
</option>
{makes.map((make) => (
<option key={make} value={make}>
{make}
</option>
))}
</select>
{nhtsaHint('make')}
</div>
{/* Model */}
<div>
<label className="block text-sm font-medium text-gray-700 dark:text-avus mb-1">
Model
</label>
<select
value={selectedModel}
onChange={(e) => handleModelChange(e.target.value)}
className={selectClasses}
style={{ fontSize: '16px' }}
disabled={loadingDropdowns || !selectedMake || models.length === 0}
>
<option value="">
{loadingDropdowns
? 'Loading...'
: !selectedMake
? 'Select make first'
: models.length === 0
? 'No models available'
: 'Select Model'}
</option>
{models.map((model) => (
<option key={model} value={model}>
{model}
</option>
))}
</select>
{nhtsaHint('model')}
</div>
{/* Trim */}
<div>
<label className="block text-sm font-medium text-gray-700 dark:text-avus mb-1">
Trim
</label>
<select
value={selectedTrim}
onChange={(e) => handleTrimChange(e.target.value)}
className={selectClasses}
style={{ fontSize: '16px' }}
disabled={loadingDropdowns || !selectedModel || trims.length === 0}
>
<option value="">
{loadingDropdowns
? 'Loading...'
: !selectedModel
? 'Select model first'
: trims.length === 0
? 'No trims available'
: 'Select Trim'}
</option>
{trims.map((trim) => (
<option key={trim} value={trim}>
{trim}
</option>
))}
</select>
{nhtsaHint('trim')}
</div>
{/* Engine */}
<div>
<label className="block text-sm font-medium text-gray-700 dark:text-avus mb-1">
Engine
</label>
<select
value={selectedEngine}
onChange={(e) => setSelectedEngine(e.target.value)}
className={selectClasses}
style={{ fontSize: '16px' }}
disabled={loadingDropdowns || !selectedTrim || engines.length === 0}
>
<option value="">
{loadingDropdowns
? 'Loading...'
: !selectedTrim
? 'Select trim first'
: engines.length === 0
? 'N/A (Electric)'
: 'Select Engine'}
</option>
{engines.map((engine) => (
<option key={engine} value={engine}>
{engine}
</option>
))}
</select>
{nhtsaHint('engine')}
</div>
{/* Transmission */}
<div>
<label className="block text-sm font-medium text-gray-700 dark:text-avus mb-1">
Transmission
</label>
<select
value={selectedTransmission}
onChange={(e) => setSelectedTransmission(e.target.value)}
className={selectClasses}
style={{ fontSize: '16px' }}
disabled={loadingDropdowns || !selectedTrim || transmissions.length === 0}
>
<option value="">
{loadingDropdowns
? 'Loading...'
: !selectedTrim
? 'Select trim first'
: transmissions.length === 0
? 'No transmissions available'
: 'Select Transmission'}
</option>
{transmissions.map((transmission) => (
<option key={transmission} value={transmission}>
{transmission}
</option>
))}
</select>
{nhtsaHint('transmission')}
</div>
</div>
</Box>
{/* No decoded data - VIN only mode */}
{!decodedVehicle && !decodeError && (
<Alert severity="info">
VIN extracted successfully. Vehicle details will need to be entered manually.
<Alert severity="info" sx={{ mb: 2 }}>
VIN extracted successfully. Select vehicle details above or enter them after accepting.
</Alert>
)}
@@ -267,20 +559,12 @@ const ReviewContent: React.FC<{
>
Retake Photo
</Button>
<Button
variant="outlined"
startIcon={<EditIcon />}
onClick={onEdit}
fullWidth
sx={{ minHeight: 44 }}
>
Edit Manually
</Button>
<Button
variant="contained"
startIcon={<CheckCircleIcon />}
onClick={onAccept}
onClick={handleAccept}
fullWidth
disabled={loadingDropdowns}
sx={{ minHeight: 44 }}
>
Accept
@@ -294,7 +578,6 @@ export const VinOcrReviewModal: React.FC<VinOcrReviewModalProps> = ({
open,
result,
onAccept,
onEdit,
onRetake,
onClose,
}) => {
@@ -318,7 +601,7 @@ export const VinOcrReviewModal: React.FC<VinOcrReviewModalProps> = ({
},
}}
>
<Box sx={{ p: 2 }}>
<Box sx={{ p: 2, overflowY: 'auto' }}>
{/* Drag handle */}
<Box
sx={{
@@ -333,12 +616,7 @@ export const VinOcrReviewModal: React.FC<VinOcrReviewModalProps> = ({
<Typography variant="h6" gutterBottom>
VIN Detected
</Typography>
<ReviewContent
result={result}
onAccept={onAccept}
onEdit={onEdit}
onRetake={onRetake}
/>
<ReviewContent result={result} onAccept={onAccept} onRetake={onRetake} />
</Box>
</Drawer>
);
@@ -357,12 +635,7 @@ export const VinOcrReviewModal: React.FC<VinOcrReviewModalProps> = ({
>
<DialogTitle>VIN Detected</DialogTitle>
<DialogContent>
<ReviewContent
result={result}
onAccept={onAccept}
onEdit={onEdit}
onRetake={onRetake}
/>
<ReviewContent result={result} onAccept={onAccept} onRetake={onRetake} />
</DialogContent>
<DialogActions sx={{ display: 'none' }}>
{/* Actions are in ReviewContent */}

View File

@@ -91,3 +91,16 @@ export interface DecodedVehicleData {
engine: MatchedField<string>;
transmission: MatchedField<string>;
}
/**
* Selections made by user in VIN OCR review modal
*/
export interface VinReviewSelections {
vin: string;
year?: number;
make?: string;
model?: string;
trimLevel?: string;
engine?: string;
transmission?: string;
}