feat: Add VIN decoding with NHTSA vPIC API (#9) #24

Merged
egullickson merged 6 commits from issue-9-vin-decoding into main 2026-01-11 22:22:36 +00:00
2 changed files with 52 additions and 37 deletions
Showing only changes of commit a6607d5882 - Show all commits

View File

@@ -441,8 +441,9 @@ export class VehiclesService {
}
/**
* Match a value against dropdown options using case-insensitive exact matching
* Match a value against dropdown options using fuzzy matching
* Returns the matched dropdown value with confidence level
* Matching order: exact -> normalized -> prefix -> contains
*/
private matchField(nhtsaValue: string, options: string[]): MatchedField<string> {
if (!nhtsaValue || options.length === 0) {
@@ -466,6 +467,18 @@ export class VehiclesService {
return { value: normalizedMatch, nhtsaValue, confidence: 'medium' };
}
// Try prefix match - option starts with NHTSA value
const prefixMatch = options.find(opt => opt.toLowerCase().trim().startsWith(normalizedNhtsa));
if (prefixMatch) {
return { value: prefixMatch, nhtsaValue, confidence: 'medium' };
}
// Try contains match - option contains NHTSA value
const containsMatch = options.find(opt => opt.toLowerCase().trim().includes(normalizedNhtsa));
if (containsMatch) {
return { value: containsMatch, nhtsaValue, confidence: 'medium' };
}
// No match found - return NHTSA value as hint with no match
return { value: null, nhtsaValue, confidence: 'none' };
}

View File

@@ -448,13 +448,45 @@ export const VehicleForm: React.FC<VehicleFormProps> = ({
isVinDecoding.current = true;
setLoadingDropdowns(true);
// Track which values we're setting (only populate empty fields)
const yearToSet = !watchedYear && decoded.year.value ? decoded.year.value : watchedYear;
const makeToSet = !watchedMake && decoded.make.value ? decoded.make.value : watchedMake;
const modelToSet = !watchedModel && decoded.model.value ? decoded.model.value : watchedModel;
const trimToSet = !watchedTrim && decoded.trimLevel.value ? decoded.trimLevel.value : watchedTrim;
// Determine final values (decoded value if field is empty, otherwise keep existing)
const yearValue = !watchedYear && decoded.year.value ? decoded.year.value : watchedYear;
const makeValue = !watchedMake && decoded.make.value ? decoded.make.value : watchedMake;
const modelValue = !watchedModel && decoded.model.value ? decoded.model.value : watchedModel;
const trimValue = !watchedTrim && decoded.trimLevel.value ? decoded.trimLevel.value : watchedTrim;
const engineValue = !watchedEngine && decoded.engine.value ? decoded.engine.value : watchedEngine;
const transmissionValue = !watchedTransmission && decoded.transmission.value ? decoded.transmission.value : watchedTransmission;
// Set form values
// FIRST: Load all dropdown options hierarchically (like edit mode initialization)
// Options must exist BEFORE setting form values for selects to display correctly
if (yearValue) {
prevYear.current = yearValue;
const makesData = await vehiclesApi.getMakes(yearValue);
setMakes(makesData);
if (makeValue) {
prevMake.current = makeValue;
const modelsData = await vehiclesApi.getModels(yearValue, makeValue);
setModels(modelsData);
if (modelValue) {
prevModel.current = modelValue;
const trimsData = await vehiclesApi.getTrims(yearValue, makeValue, modelValue);
setTrims(trimsData);
if (trimValue) {
prevTrim.current = trimValue;
const [enginesData, transmissionsData] = await Promise.all([
vehiclesApi.getEngines(yearValue, makeValue, modelValue, trimValue),
vehiclesApi.getTransmissions(yearValue, makeValue, modelValue, trimValue)
]);
setEngines(enginesData);
setTransmissions(transmissionsData);
}
}
}
}
// THEN: Set form values (after options are loaded)
if (!watchedYear && decoded.year.value) {
setValue('year', decoded.year.value);
}
@@ -474,36 +506,6 @@ export const VehicleForm: React.FC<VehicleFormProps> = ({
setValue('transmission', decoded.transmission.value);
}
// Load dropdown options hierarchically (like edit mode initialization)
// This ensures dropdowns have the right options for the decoded values
if (yearToSet) {
prevYear.current = yearToSet;
const makesData = await vehiclesApi.getMakes(yearToSet);
setMakes(makesData);
if (makeToSet) {
prevMake.current = makeToSet;
const modelsData = await vehiclesApi.getModels(yearToSet, makeToSet);
setModels(modelsData);
if (modelToSet) {
prevModel.current = modelToSet;
const trimsData = await vehiclesApi.getTrims(yearToSet, makeToSet, modelToSet);
setTrims(trimsData);
if (trimToSet) {
prevTrim.current = trimToSet;
const [enginesData, transmissionsData] = await Promise.all([
vehiclesApi.getEngines(yearToSet, makeToSet, modelToSet, trimToSet),
vehiclesApi.getTransmissions(yearToSet, makeToSet, modelToSet, trimToSet)
]);
setEngines(enginesData);
setTransmissions(transmissionsData);
}
}
}
}
setLoadingDropdowns(false);
isVinDecoding.current = false;
} catch (error: any) {