fix(emex): improve wizard vehicle matching with model-based fuzzy search
Some checks failed
CI / Lint, Typecheck, Test & Build (push) Has been cancelled
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:
@@ -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}
|
||||
|
||||
Reference in New Issue
Block a user