feat: EMEX HTTP decode + multi-candidate selection UI
Some checks failed
CI / Lint, Typecheck, Test & Build (push) Has been cancelled
Deploy / Deploy to Production (push) Has been cancelled

Replace Playwright browser with pure HTTP for EMEX VIN decode:
- Vehicles.aspx, QuickGroups.aspx, QuickDetails.aspx all work
  without authentication — no session/browser needed
- VIN decode drops from ~10-20s (Playwright) to ~1-5s (HTTP)
- Playwright browser kept as fallback if HTTP fails

Add multi-candidate selection for EMEX (mirrors PartsCatalogs flow):
- EmexCandidate interface matches PcatCar shape so VehicleSelectModal
  renders without changes
- decodeVinOrCandidates(): single HTTP fetch returns vehicle OR
  candidates (avoids double Vehicles.aspx request)
- decodeVinByIndex(): resolves user-selected candidate by index
- vehicles.service: emexCandidates in VinResolveResult, emexCarIndex
  param in decodeVin/resolveVin, resolveEmexCarByIndex() method
- vehicles.controller: @Body("emexCarIndex") accepted
- search.tsx: candidateSource state, handleCandidateSelect sends
  emexCarIndex or pcatCarId based on source

Test VIN NM417800006410193 (Fiat SIENA) now shows 2-option modal.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
Sase Dev
2026-03-05 09:46:25 +00:00
parent f29a923e6e
commit ce8784014e
5 changed files with 610 additions and 83 deletions

View File

@@ -52,9 +52,10 @@ function SearchPage() {
const [reportSending, setReportSending] = useState(false);
const [reportSent, setReportSent] = useState(false);
// Vehicle candidate selection (PartsCatalogs multi-result)
// Vehicle candidate selection (PartsCatalogs or EMEX multi-result)
const [candidates, setCandidates] = useState<any[] | null>(null);
const [candidateVin, setCandidateVin] = useState("");
const [candidateSource, setCandidateSource] = useState<"parts-catalogs" | "emex" | null>(null);
const [selectLoading, setSelectLoading] = useState(false);
// Live preview state
@@ -149,13 +150,15 @@ function SearchPage() {
try {
const data = await api.post<any>("/vehicles/decode", { vin: cleanVin });
// Handle multiple vehicle candidates (PartsCatalogs)
// Handle multiple vehicle candidates (PartsCatalogs or EMEX)
if (data.candidates && Array.isArray(data.candidates)) {
setCandidates(data.candidates);
setCandidateVin(cleanVin);
setCandidateSource(data.source ?? "parts-catalogs");
capture("vin_decode_candidates", {
vin: cleanVin,
count: data.candidates.length,
source: data.source,
});
setLoading(false);
return;
@@ -209,19 +212,24 @@ function SearchPage() {
}
}
async function handleCandidateSelect(pcatCarId: string) {
async function handleCandidateSelect(carId: string) {
setSelectLoading(true);
try {
const data = await api.post<any>("/vehicles/decode", {
vin: candidateVin,
pcatCarId,
});
const payload: Record<string, unknown> = { vin: candidateVin };
if (candidateSource === "emex") {
payload.emexCarIndex = parseInt(carId, 10);
} else {
payload.pcatCarId = carId;
}
const data = await api.post<any>("/vehicles/decode", payload);
capture("vin_decode_candidate_selected", {
vin: candidateVin,
pcatCarId,
source: candidateSource,
carId,
vehicle_id: data.id,
});
setCandidates(null);
setCandidateSource(null);
navigate({
to: "/dashboard/vehicles/$id",
params: { id: data.id },
@@ -233,6 +241,7 @@ function SearchPage() {
: "Bir hata oluştu. Lütfen tekrar deneyin.";
setError(message);
setCandidates(null);
setCandidateSource(null);
toast.error("Araç seçimi başarısız");
} finally {
setSelectLoading(false);
@@ -474,7 +483,7 @@ function SearchPage() {
</div>
)}
{/* ─── Vehicle Selection Modal (PartsCatalogs multi-result) ──── */}
{/* ─── Vehicle Selection Modal (PartsCatalogs / EMEX multi-result) ──── */}
{candidates && (
<VehicleSelectModal
open={!!candidates}