feat: service-test EMEX flow — candidate modal + vehicle card + auto-navigate
Some checks failed
CI / Lint, Typecheck, Test & Build (push) Has been cancelled
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:
@@ -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>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user