feat: improve VIN confidence reporting and editable review dropdowns (refs #125)
All checks were successful
Deploy to Staging / Build Images (pull_request) Successful in 4m37s
Deploy to Staging / Deploy to Staging (pull_request) Successful in 51s
Deploy to Staging / Verify Staging (pull_request) Successful in 9s
Deploy to Staging / Notify Staging Ready (pull_request) Successful in 8s
Deploy to Staging / Notify Staging Failure (pull_request) Has been skipped

VIN OCR confidence now reflects recognition accuracy only (not match quality).
Review modal replaces read-only fields with editable cascade dropdowns
pre-populated from NHTSA decode, with NHTSA reference hints for unmatched fields.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
Eric Gullickson
2026-02-08 19:24:27 -06:00
parent e7471d5c27
commit e9020dbb2f
3 changed files with 486 additions and 229 deletions

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}
/>