fix(emex): improve wizard vehicle matching with model-based fuzzy search
Some checks failed
CI / Lint, Typecheck, Test & Build (push) Has been cancelled

- Backend: add `model` query param to wizard-vehicles endpoint
- Backend: 3-tier matching: exact name → name prefix → model fuzzy
- Frontend: simplified wizard UI, show determined params as tags

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-03-06 09:50:52 +00:00
parent 184bf4741e
commit 4e838fdace
3 changed files with 107 additions and 125 deletions

View File

@@ -55,10 +55,6 @@ function EmexVehicleListPage() {
// Current SSD state for wizard navigation
const [ssd, setSsd] = useState("");
// History of SSD selections for display
const [selections, setSelections] = useState<
{ name: string; value: string; ssd: string }[]
>([]);
// Fetch wizard data for current SSD
const {
@@ -79,18 +75,20 @@ function EmexVehicleListPage() {
[wizardRows],
);
const undetermined = useMemo(
() => wizardRows?.filter((r) => !r.determined && r.options?.length > 0) ?? [],
() =>
wizardRows?.filter((r) => !r.determined && r.options?.length > 0) ?? [],
[wizardRows],
);
const allDetermined = wizardRows
? wizardRows.length > 0 && undetermined.length === 0
: false;
// Get the "Sales Designation" or "Name" from determined params
// to match against DB vehicles
const salesDesignation = useMemo(() => {
// Get the "Sales Designation" and "Model" from determined params
const wizardMatch = useMemo(() => {
if (!allDetermined || !determined.length) return null;
// Try known name fields
let salesDesignation: string | null = null;
let model: string | null = null;
for (const key of [
"Sales Designation",
"Name",
@@ -98,46 +96,49 @@ function EmexVehicleListPage() {
"Model name",
]) {
const row = determined.find((r) => r.name === key);
if (row?.value && row.value !== "None") return row.value;
if (row?.value && row.value !== "None") {
salesDesignation = row.value;
break;
}
}
return null;
const modelRow = determined.find((r) => r.name === "Model");
if (modelRow?.value && modelRow.value !== "None") {
model = modelRow.value;
}
const name = salesDesignation || model;
if (!name) return null;
return { name, model };
}, [allDetermined, determined]);
// When all wizard params are determined, search DB for matching vehicles
const { data: matchedVehicles, isLoading: matchLoading } = useQuery({
queryKey: ["emex-wizard-vehicles", catalogCode, salesDesignation],
queryFn: () =>
api.get<EmexVehicle[]>(
`/catalog/emex/brands/${catalogCode}/wizard-vehicles?name=${encodeURIComponent(salesDesignation!)}`,
),
enabled: !!salesDesignation,
queryKey: [
"emex-wizard-vehicles",
catalogCode,
wizardMatch?.name,
wizardMatch?.model,
],
queryFn: () => {
const params = new URLSearchParams({ name: wizardMatch!.name });
if (wizardMatch!.model) params.set("model", wizardMatch!.model);
return api.get<EmexVehicle[]>(
`/catalog/emex/brands/${catalogCode}/wizard-vehicles?${params}`,
);
},
enabled: !!wizardMatch,
});
// Handle wizard option selection
const handleSelect = useCallback(
(rowName: string, option: WizardOption) => {
setSelections((prev) => [...prev, { name: rowName, value: option.value, ssd }]);
setSsd(option.key);
},
[ssd],
);
// Handle wizard option selection — navigate to new SSD
const handleSelect = useCallback((_rowName: string, option: WizardOption) => {
setSsd(option.key);
}, []);
// Reset wizard to initial state
const handleReset = useCallback(() => {
setSsd("");
setSelections([]);
}, []);
// Go back to a previous step
const handleBack = useCallback(
(stepIndex: number) => {
const target = selections[stepIndex];
setSsd(target.ssd);
setSelections((prev) => prev.slice(0, stepIndex));
},
[selections],
);
return (
<div className="space-y-4">
{/* Header */}
@@ -151,7 +152,7 @@ function EmexVehicleListPage() {
<h1 className="text-xl font-bold">
{decodeURIComponent(catalogCode)}
</h1>
{selections.length > 0 && (
{determined.length > 0 && (
<Button variant="ghost" size="sm" onClick={handleReset}>
<RotateCcw className="mr-1 size-3.5" />
{t("catalog.resetSelection")}
@@ -159,24 +160,18 @@ function EmexVehicleListPage() {
)}
</div>
{/* Breadcrumb: show selected wizard steps */}
{selections.length > 0 && (
<div className="flex flex-wrap items-center gap-1 text-sm">
{selections.map((sel, i) => (
<span key={i} className="flex items-center gap-1">
<button
type="button"
onClick={() => handleBack(i)}
className="rounded px-1.5 py-0.5 text-muted-foreground hover:bg-accent hover:text-foreground"
>
{sel.value}
</button>
<ChevronRight className="size-3 text-muted-foreground" />
{/* Determined params — shown as tags */}
{determined.length > 0 && (
<div className="flex flex-wrap gap-2">
{determined.map((r) => (
<span
key={r.name}
className="rounded-md border border-border bg-muted/50 px-2 py-1 text-xs"
>
<span className="text-muted-foreground">{r.name}:</span>{" "}
{r.value}
</span>
))}
{allDetermined && salesDesignation && (
<span className="font-medium">{salesDesignation}</span>
)}
</div>
)}
@@ -191,40 +186,20 @@ function EmexVehicleListPage() {
{/* Loading */}
{wizardLoading && (
<div className="space-y-2">
{Array.from({ length: 4 }).map((_, i) => (
{Array.from({ length: 3 }).map((_, i) => (
<Skeleton key={i} className="h-10 w-full rounded-lg" />
))}
</div>
)}
{/* Wizard: show undetermined rows as selection lists */}
{/* Wizard: show first undetermined row as clickable list */}
{!wizardLoading && !wizardError && !allDetermined && undetermined.length > 0 && (
<WizardStep
row={undetermined[0]}
onSelect={handleSelect}
/>
<WizardStep row={undetermined[0]} onSelect={handleSelect} />
)}
{/* All determined: show matched vehicles from DB */}
{allDetermined && (
<div className="space-y-3">
<div className="rounded-lg border border-border bg-muted/30 p-3">
<p className="mb-2 text-xs font-medium text-muted-foreground">
Seçilen araç parametreleri
</p>
<div className="flex flex-wrap gap-2">
{determined.map((r) => (
<span
key={r.name}
className="rounded-md bg-background px-2 py-1 text-xs"
>
<span className="text-muted-foreground">{r.name}:</span>{" "}
{r.value}
</span>
))}
</div>
</div>
{matchLoading ? (
<div className="space-y-2">
{Array.from({ length: 3 }).map((_, i) => (
@@ -258,7 +233,7 @@ function EmexVehicleListPage() {
</div>
)}
{/* Empty state: no wizard rows */}
{/* Empty state: no wizard rows at all */}
{!wizardLoading &&
!wizardError &&
wizardRows &&
@@ -297,7 +272,7 @@ function WizardStep({
</span>
</div>
{row.options.length > 8 && (
{row.options.length > 10 && (
<input
type="text"
value={search}