feat: service-test EMEX flow — candidate modal + vehicle card + auto-navigate
Some checks failed
CI / Lint, Typecheck, Test & Build (push) Has been cancelled

When EMEX returns candidates, show VehicleSelectModal for selection.
When EMEX returns a single vehicle, show vehicle info card and
auto-navigate to the category page via /vehicles/decode.
Failed results still show JSON output as before.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-03-06 13:05:28 +00:00
parent 1a552ed8fd
commit 8bbd82bc2c

View File

@@ -1,5 +1,5 @@
import { useState, useEffect, useRef } from "react"; import { useState, useEffect, useRef } from "react";
import { createFileRoute } from "@tanstack/react-router"; import { createFileRoute, useNavigate } from "@tanstack/react-router";
import { Button, Input, Badge, Separator } from "@sase/ui"; import { Button, Input, Badge, Separator } from "@sase/ui";
import { import {
FlaskConical, FlaskConical,
@@ -8,9 +8,11 @@ import {
AlertCircle, AlertCircle,
Copy, Copy,
Check, Check,
Car,
} from "lucide-react"; } from "lucide-react";
import { api, ApiError } from "@/lib/api-client"; import { api, ApiError } from "@/lib/api-client";
import { toast } from "@/lib/toast"; import { toast } from "@/lib/toast";
import { VehicleSelectModal } from "@/components/vehicles/vehicle-select-modal";
// ─── HELPERS ────────────────────────────────────────────────────────────────── // ─── HELPERS ──────────────────────────────────────────────────────────────────
@@ -48,6 +50,7 @@ export const Route = createFileRoute("/dashboard/service-test")({
}); });
function ServiceTestPage() { function ServiceTestPage() {
const navigate = useNavigate();
const inputRef = useRef<HTMLInputElement>(null); const inputRef = useRef<HTMLInputElement>(null);
const [vin, setVin] = useState(""); const [vin, setVin] = useState("");
const [service, setService] = useState("all"); const [service, setService] = useState("all");
@@ -62,6 +65,11 @@ function ServiceTestPage() {
} | null>(null); } | null>(null);
const [copied, setCopied] = useState(false); const [copied, setCopied] = useState(false);
// EMEX candidate selection state
const [candidates, setCandidates] = useState<any[] | null>(null);
const [candidateVin, setCandidateVin] = useState("");
const [selectLoading, setSelectLoading] = useState(false);
useEffect(() => { useEffect(() => {
inputRef.current?.focus(); inputRef.current?.focus();
}, []); }, []);
@@ -98,6 +106,33 @@ function ServiceTestPage() {
vin: cleanVin, vin: cleanVin,
service: service === "all" ? undefined : service, service: service === "all" ? undefined : service,
}); });
// EMEX success with candidates → show selection modal
if (data.success && data.result?.type === "candidates" && data.result.candidates) {
setCandidateVin(cleanVin);
setCandidates(data.result.candidates);
setResult(data);
setLoading(false);
return;
}
// EMEX success with single vehicle → navigate to decode flow
if (data.success && data.result?.type === "vehicle" && data.result.vehicle) {
setResult(data);
setLoading(false);
// Auto-decode via normal flow
try {
const decoded = await api.post<any>("/vehicles/decode", { vin: cleanVin });
if (decoded.id) {
navigate({ to: "/dashboard/vehicles/$id", params: { id: decoded.id } });
return;
}
} catch {
// Fall through to show JSON result
}
return;
}
setResult(data); setResult(data);
} catch (err) { } catch (err) {
const message = err instanceof ApiError ? err.message : "Bir hata oluştu. Lütfen tekrar deneyin."; const message = err instanceof ApiError ? err.message : "Bir hata oluştu. Lütfen tekrar deneyin.";
@@ -120,6 +155,27 @@ function ServiceTestPage() {
} }
} }
async function handleCandidateSelect(carId: string) {
setSelectLoading(true);
try {
const data = await api.post<any>("/vehicles/decode", {
vin: candidateVin,
emexCarIndex: parseInt(carId, 10),
});
setCandidates(null);
if (data.id) {
navigate({ to: "/dashboard/vehicles/$id", params: { id: data.id } });
}
} catch (err) {
const message = err instanceof ApiError ? err.message : "Bir hata oluştu.";
setError(message);
setCandidates(null);
toast.error("Araç seçimi başarısız");
} finally {
setSelectLoading(false);
}
}
function fillExampleVin() { function fillExampleVin() {
setVin("WVWZZZ1JZ3W597935"); setVin("WVWZZZ1JZ3W597935");
inputRef.current?.focus(); inputRef.current?.focus();
@@ -242,8 +298,47 @@ function ServiceTestPage() {
</form> </form>
</div> </div>
{/* ─── SECTION 2: Result Card ──────────────────────────────────────── */} {/* ─── SECTION 2: Vehicle Info Card (successful single decode) ──── */}
{result && ( {result?.success && result.result?.type === "vehicle" && result.result.vehicle && (
<div className="rounded-2xl border border-emerald-500/30 bg-background p-5 sm:p-6">
<div className="flex items-start gap-4">
<div className="flex size-10 shrink-0 items-center justify-center rounded-xl bg-emerald-500/10">
<Car className="size-5 text-emerald-500" />
</div>
<div className="min-w-0 flex-1">
<p className="font-[family-name:var(--font-display)] text-lg font-bold">
{result.result.vehicle.brand} {result.result.vehicle.model}
</p>
<p className="mt-0.5 text-sm text-muted-foreground">
{result.result.vehicle.year || "—"}
{result.result.vehicle.engineCode && ` — Motor: ${result.result.vehicle.engineCode}`}
</p>
<div className="mt-3 flex flex-wrap gap-2">
<Badge variant="default" className="bg-emerald-600 text-xs text-white">
Araç tanımlandı
</Badge>
<Badge variant="secondary" className="text-xs">
{result.service}
</Badge>
<Badge variant="secondary" className="text-xs">
{result.responseTimeMs.toLocaleString("tr-TR")}ms
</Badge>
</div>
</div>
</div>
{result.result.vehicle.categories?.length > 0 && (
<>
<Separator className="my-4 bg-border" />
<p className="mb-2 text-sm font-medium text-muted-foreground">
{result.result.vehicle.categories.length} kategori bulundu
</p>
</>
)}
</div>
)}
{/* ─── SECTION 3: Result Card (JSON for failures or non-vehicle results) ── */}
{result && (!result.success || result.result?.type !== "vehicle") && result.result?.type !== "candidates" && (
<div className="rounded-2xl border border-border bg-background p-5 sm:p-6"> <div className="rounded-2xl border border-border bg-background p-5 sm:p-6">
{/* Meta badges */} {/* Meta badges */}
<div className="flex flex-wrap items-center gap-2"> <div className="flex flex-wrap items-center gap-2">
@@ -297,6 +392,18 @@ function ServiceTestPage() {
)} )}
</div> </div>
)} )}
{/* ─── Vehicle Selection Modal (EMEX multi-result) ──────────────── */}
{candidates && (
<VehicleSelectModal
open={!!candidates}
onClose={() => setCandidates(null)}
candidates={candidates}
vin={candidateVin}
onSelect={handleCandidateSelect}
loading={selectLoading}
/>
)}
</div> </div>
); );
} }