feat(emex): wizard-based vehicle selection via GetWizard2 API
Some checks failed
CI / Lint, Typecheck, Test & Build (push) Has been cancelled
Some checks failed
CI / Lint, Typecheck, Test & Build (push) Has been cancelled
Replace static DB-based dropdown chain with live wizard from emexdwc.ae. The wizard API (GetWizard2) works without auth and shows all models/options for each catalog (e.g. AU1587 now shows 103 Audi models vs 11 before). Backend: proxy GetWizard2 + search DB for matching vehicles after wizard completes. Frontend: step-by-step wizard with breadcrumb navigation, search, and reset. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -4,11 +4,33 @@ import { useQuery } from "@tanstack/react-query";
|
||||
import { api } from "@/lib/api-client";
|
||||
import { useTranslation } from "@/lib/i18n";
|
||||
import { Button, Skeleton } from "@sase/ui";
|
||||
import { ArrowLeft, Car, ChevronRight } from "lucide-react";
|
||||
import {
|
||||
ArrowLeft,
|
||||
Car,
|
||||
ChevronRight,
|
||||
RotateCcw,
|
||||
AlertCircle,
|
||||
} from "lucide-react";
|
||||
|
||||
export const Route = createFileRoute("/dashboard/catalog_/emex/$catalogCode/")({
|
||||
component: EmexVehicleListPage,
|
||||
});
|
||||
export const Route = createFileRoute("/dashboard/catalog_/emex/$catalogCode/")(
|
||||
{
|
||||
component: EmexVehicleListPage,
|
||||
},
|
||||
);
|
||||
|
||||
// ── Types ────────────────────────────────────────────
|
||||
|
||||
interface WizardRow {
|
||||
name: string;
|
||||
value: string | null;
|
||||
determined: boolean;
|
||||
options: WizardOption[];
|
||||
}
|
||||
|
||||
interface WizardOption {
|
||||
key: string;
|
||||
value: string;
|
||||
}
|
||||
|
||||
interface EmexVehicle {
|
||||
id: string;
|
||||
@@ -25,249 +47,100 @@ interface EmexVehicle {
|
||||
optionsRaw: string | null;
|
||||
}
|
||||
|
||||
// ── Helpers ──────────────────────────────────────────
|
||||
|
||||
/** Parse "Key: Value;Key2: Value2" into a map */
|
||||
function parseOptions(raw: string | null): Record<string, string> {
|
||||
if (!raw) return {};
|
||||
const map: Record<string, string> = {};
|
||||
for (const part of raw.split(";")) {
|
||||
const idx = part.indexOf(":");
|
||||
if (idx > 0) {
|
||||
map[part.slice(0, idx).trim()] = part.slice(idx + 1).trim();
|
||||
}
|
||||
}
|
||||
return map;
|
||||
}
|
||||
|
||||
function hasStructuredOptions(vehicles: EmexVehicle[]): boolean {
|
||||
const sample = vehicles.find((v) => v.optionsRaw);
|
||||
return sample?.optionsRaw?.includes(":") ?? false;
|
||||
}
|
||||
|
||||
function distinctValues(
|
||||
vehicles: EmexVehicle[],
|
||||
getter: (v: EmexVehicle) => string | null | undefined,
|
||||
): string[] {
|
||||
const set = new Set<string>();
|
||||
for (const v of vehicles) {
|
||||
const val = getter(v);
|
||||
if (val) set.add(val);
|
||||
}
|
||||
return [...set].sort();
|
||||
}
|
||||
|
||||
// ── Dynamic filter builder ───────────────────────────
|
||||
|
||||
interface FilterDef {
|
||||
key: string;
|
||||
label: string;
|
||||
getter: (v: EmexVehicle) => string | null | undefined;
|
||||
}
|
||||
|
||||
/**
|
||||
* Build filter chain dynamically from what the data actually contains.
|
||||
* For structured options_raw (Renault/Dacia/Toyota/Lexus), parse the key:value pairs.
|
||||
* For others, use DB fields in a sensible order and skip optionsRaw if it
|
||||
* duplicates another field or is the only remaining differentiator (shown as row text).
|
||||
*/
|
||||
function buildFilterDefs(
|
||||
vehicles: EmexVehicle[],
|
||||
structured: boolean,
|
||||
): FilterDef[] {
|
||||
if (structured) {
|
||||
return buildStructuredFilters(vehicles);
|
||||
}
|
||||
return buildFieldFilters(vehicles);
|
||||
}
|
||||
|
||||
function buildStructuredFilters(vehicles: EmexVehicle[]): FilterDef[] {
|
||||
// Collect all option keys and count usage
|
||||
const keyCount = new Map<string, number>();
|
||||
for (const v of vehicles) {
|
||||
for (const k of Object.keys(parseOptions(v.optionsRaw))) {
|
||||
keyCount.set(k, (keyCount.get(k) || 0) + 1);
|
||||
}
|
||||
}
|
||||
|
||||
const defs: FilterDef[] = [];
|
||||
const labelMap: Record<string, string> = {
|
||||
"Engine type": "Motor",
|
||||
"Gearbox type": "Şanzıman",
|
||||
"Vehicle type": "Araç Tipi",
|
||||
};
|
||||
|
||||
for (const [optKey, label] of Object.entries(labelMap)) {
|
||||
if (keyCount.has(optKey)) {
|
||||
const k = optKey;
|
||||
defs.push({
|
||||
key: k.replace(/\s+/g, "_"),
|
||||
label,
|
||||
getter: (v) => parseOptions(v.optionsRaw)[k] || null,
|
||||
});
|
||||
}
|
||||
}
|
||||
return defs;
|
||||
}
|
||||
|
||||
function buildFieldFilters(vehicles: EmexVehicle[]): FilterDef[] {
|
||||
// Candidate fields in priority order
|
||||
const candidates: {
|
||||
key: string;
|
||||
label: string;
|
||||
getter: (v: EmexVehicle) => string | null | undefined;
|
||||
}[] = [
|
||||
{ key: "engine", label: "Motor", getter: (v) => v.engine },
|
||||
{ key: "bodyType", label: "Kasa Tipi", getter: (v) => v.bodyType },
|
||||
{ key: "transmission", label: "Şanzıman", getter: (v) => v.transmission },
|
||||
{ key: "driveType", label: "Sürüş", getter: (v) => v.driveType },
|
||||
{ key: "fuelType", label: "Yakıt", getter: (v) => v.fuelType },
|
||||
{ key: "optionsRaw", label: "Varyant", getter: (v) => v.optionsRaw },
|
||||
];
|
||||
|
||||
// Collect distinct values per field
|
||||
const fieldVals = new Map<string, string[]>();
|
||||
for (const c of candidates) {
|
||||
const vals = distinctValues(vehicles, c.getter);
|
||||
if (vals.length >= 2) fieldVals.set(c.key, vals);
|
||||
}
|
||||
|
||||
const optRawVals = fieldVals.get("optionsRaw") || [];
|
||||
// Pre-compute optionsRaw values as lowercase set for containment check
|
||||
const optRawLower = new Set(
|
||||
vehicles
|
||||
.map((v) => v.optionsRaw?.toLowerCase())
|
||||
.filter((x): x is string => !!x),
|
||||
);
|
||||
|
||||
const defs: FilterDef[] = [];
|
||||
for (const c of candidates) {
|
||||
if (!fieldVals.has(c.key)) continue;
|
||||
const vals = fieldVals.get(c.key)!;
|
||||
const valSet = new Set(vals);
|
||||
|
||||
// Skip if this field's distinct values exactly match an already-added field
|
||||
// (e.g. engine="LHD/RHD" and driveType="LHD/RHD")
|
||||
const isDuplicateOfPrev = defs.some((d) => {
|
||||
const dVals = fieldVals.get(d.key) || [];
|
||||
return (
|
||||
dVals.length === vals.length && dVals.every((v) => valSet.has(v))
|
||||
);
|
||||
});
|
||||
if (isDuplicateOfPrev) continue;
|
||||
|
||||
// Skip optionsRaw if its values exactly match another already-added field
|
||||
if (c.key === "optionsRaw" && isDuplicateOfPrev) continue;
|
||||
|
||||
// Skip a field if every one of its values is a substring of the
|
||||
// corresponding vehicle's optionsRaw (e.g. driveType="LEFT" inside
|
||||
// optionsRaw="LEFT (GSX)"). The optionsRaw filter alone is enough.
|
||||
if (c.key !== "optionsRaw" && optRawVals.length >= 2) {
|
||||
const isRedundant = vehicles.every((v) => {
|
||||
const fv = c.getter(v);
|
||||
const ov = v.optionsRaw;
|
||||
if (!fv) return true; // no value → not contradicting
|
||||
if (!ov) return false; // field has value but no optionsRaw → keep field
|
||||
return ov.toLowerCase().includes(fv.toLowerCase());
|
||||
});
|
||||
if (isRedundant) continue;
|
||||
}
|
||||
|
||||
defs.push(c);
|
||||
}
|
||||
|
||||
return defs;
|
||||
}
|
||||
|
||||
// ── Component ────────────────────────────────────────
|
||||
|
||||
function EmexVehicleListPage() {
|
||||
const { t } = useTranslation();
|
||||
const { catalogCode } = Route.useParams();
|
||||
|
||||
const { data: vehicles, isLoading } = useQuery({
|
||||
queryKey: ["emex-vehicles", catalogCode],
|
||||
// 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 {
|
||||
data: wizardRows,
|
||||
isLoading: wizardLoading,
|
||||
isError: wizardError,
|
||||
} = useQuery({
|
||||
queryKey: ["emex-wizard", catalogCode, ssd],
|
||||
queryFn: () =>
|
||||
api.get<EmexVehicle[]>(`/catalog/emex/brands/${catalogCode}/vehicles`),
|
||||
api.get<WizardRow[]>(
|
||||
`/catalog/emex/brands/${catalogCode}/wizard?ssd=${encodeURIComponent(ssd)}`,
|
||||
),
|
||||
});
|
||||
|
||||
const structured = useMemo(
|
||||
() => (vehicles ? hasStructuredOptions(vehicles) : false),
|
||||
[vehicles],
|
||||
// Parse wizard state
|
||||
const determined = useMemo(
|
||||
() => wizardRows?.filter((r) => r.determined) ?? [],
|
||||
[wizardRows],
|
||||
);
|
||||
|
||||
const allFilterDefs = useMemo(
|
||||
() => (vehicles ? buildFilterDefs(vehicles, structured) : []),
|
||||
[vehicles, structured],
|
||||
const undetermined = useMemo(
|
||||
() => wizardRows?.filter((r) => !r.determined && r.options?.length > 0) ?? [],
|
||||
[wizardRows],
|
||||
);
|
||||
const allDetermined = wizardRows
|
||||
? wizardRows.length > 0 && undetermined.length === 0
|
||||
: false;
|
||||
|
||||
const modelNames = useMemo(
|
||||
() => (vehicles ? distinctValues(vehicles, (v) => v.name) : []),
|
||||
[vehicles],
|
||||
);
|
||||
|
||||
const [selectedModel, setSelectedModel] = useState("");
|
||||
const [filters, setFilters] = useState<Record<string, string>>({});
|
||||
|
||||
const modelVehicles = useMemo(
|
||||
() =>
|
||||
selectedModel && vehicles
|
||||
? vehicles.filter((v) => v.name === selectedModel)
|
||||
: [],
|
||||
[vehicles, selectedModel],
|
||||
);
|
||||
|
||||
// Cascading filter chain: compute options per filter, narrowing the pool
|
||||
const filterChain = useMemo(() => {
|
||||
const chain: { def: FilterDef; options: string[] }[] = [];
|
||||
let pool = modelVehicles;
|
||||
|
||||
for (const def of allFilterDefs) {
|
||||
const opts = distinctValues(pool, def.getter);
|
||||
if (opts.length > 1) {
|
||||
chain.push({ def, options: opts });
|
||||
}
|
||||
const selected = filters[def.key];
|
||||
if (selected) {
|
||||
pool = pool.filter((v) => def.getter(v) === selected);
|
||||
}
|
||||
// Get the "Sales Designation" or "Name" from determined params
|
||||
// to match against DB vehicles
|
||||
const salesDesignation = useMemo(() => {
|
||||
if (!allDetermined || !determined.length) return null;
|
||||
// Try known name fields
|
||||
for (const key of [
|
||||
"Sales Designation",
|
||||
"Name",
|
||||
"Modification",
|
||||
"Model name",
|
||||
]) {
|
||||
const row = determined.find((r) => r.name === key);
|
||||
if (row?.value && row.value !== "None") return row.value;
|
||||
}
|
||||
return chain;
|
||||
}, [modelVehicles, allFilterDefs, filters]);
|
||||
return null;
|
||||
}, [allDetermined, determined]);
|
||||
|
||||
// Final filtered list
|
||||
const filteredVehicles = useMemo(() => {
|
||||
let list = modelVehicles;
|
||||
for (const def of allFilterDefs) {
|
||||
const selected = filters[def.key];
|
||||
if (selected) {
|
||||
list = list.filter((v) => def.getter(v) === selected);
|
||||
}
|
||||
}
|
||||
return list;
|
||||
}, [modelVehicles, allFilterDefs, filters]);
|
||||
// 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,
|
||||
});
|
||||
|
||||
const handleModelChange = useCallback((name: string) => {
|
||||
setSelectedModel(name);
|
||||
setFilters({});
|
||||
// Handle wizard option selection
|
||||
const handleSelect = useCallback(
|
||||
(rowName: string, option: WizardOption) => {
|
||||
setSelections((prev) => [...prev, { name: rowName, value: option.value, ssd }]);
|
||||
setSsd(option.key);
|
||||
},
|
||||
[ssd],
|
||||
);
|
||||
|
||||
// Reset wizard to initial state
|
||||
const handleReset = useCallback(() => {
|
||||
setSsd("");
|
||||
setSelections([]);
|
||||
}, []);
|
||||
|
||||
const handleFilterChange = useCallback(
|
||||
(key: string, value: string) => {
|
||||
const idx = allFilterDefs.findIndex((d) => d.key === key);
|
||||
const newFilters: Record<string, string> = {};
|
||||
for (let i = 0; i < idx; i++) {
|
||||
const k = allFilterDefs[i].key;
|
||||
if (filters[k]) newFilters[k] = filters[k];
|
||||
}
|
||||
if (value) newFilters[key] = value;
|
||||
setFilters(newFilters);
|
||||
// Go back to a previous step
|
||||
const handleBack = useCallback(
|
||||
(stepIndex: number) => {
|
||||
const target = selections[stepIndex];
|
||||
setSsd(target.ssd);
|
||||
setSelections((prev) => prev.slice(0, stepIndex));
|
||||
},
|
||||
[allFilterDefs, filters],
|
||||
[selections],
|
||||
);
|
||||
|
||||
return (
|
||||
<div className="space-y-4">
|
||||
{/* Header */}
|
||||
<div className="flex items-center gap-3">
|
||||
<Link to="/dashboard/catalog" search={{}}>
|
||||
<Button variant="ghost" size="sm">
|
||||
@@ -278,107 +151,180 @@ function EmexVehicleListPage() {
|
||||
<h1 className="text-xl font-bold">
|
||||
{decodeURIComponent(catalogCode)}
|
||||
</h1>
|
||||
{selections.length > 0 && (
|
||||
<Button variant="ghost" size="sm" onClick={handleReset}>
|
||||
<RotateCcw className="mr-1 size-3.5" />
|
||||
{t("catalog.resetSelection")}
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{isLoading ? (
|
||||
{/* 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" />
|
||||
</span>
|
||||
))}
|
||||
{allDetermined && salesDesignation && (
|
||||
<span className="font-medium">{salesDesignation}</span>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Error state */}
|
||||
{wizardError && (
|
||||
<div className="flex items-center gap-2 rounded-lg border border-destructive/50 bg-destructive/10 p-4 text-sm text-destructive">
|
||||
<AlertCircle className="size-4 shrink-0" />
|
||||
<p>Katalog verileri yüklenemedi. Lütfen tekrar deneyin.</p>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Loading */}
|
||||
{wizardLoading && (
|
||||
<div className="space-y-2">
|
||||
{Array.from({ length: 4 }).map((_, i) => (
|
||||
<Skeleton key={i} className="h-12 w-full rounded-lg" />
|
||||
<Skeleton key={i} className="h-10 w-full rounded-lg" />
|
||||
))}
|
||||
</div>
|
||||
) : !vehicles || vehicles.length === 0 ? (
|
||||
<p className="py-8 text-center text-muted-foreground">
|
||||
{t("catalog.noModels")}
|
||||
</p>
|
||||
) : (
|
||||
<>
|
||||
{/* Dropdown chain */}
|
||||
<div className="flex flex-wrap gap-3">
|
||||
<FilterSelect
|
||||
label={t("catalog.models")}
|
||||
placeholder={t("catalog.selectModel")}
|
||||
value={selectedModel}
|
||||
options={modelNames}
|
||||
onChange={handleModelChange}
|
||||
/>
|
||||
)}
|
||||
|
||||
{selectedModel &&
|
||||
filterChain.map(({ def, options }) => (
|
||||
<FilterSelect
|
||||
key={def.key}
|
||||
label={def.label}
|
||||
placeholder={t("catalog.fordVariant.showAll")}
|
||||
value={filters[def.key] || ""}
|
||||
options={options}
|
||||
onChange={(val) => handleFilterChange(def.key, val)}
|
||||
/>
|
||||
{/* Wizard: show undetermined rows as selection lists */}
|
||||
{!wizardLoading && !wizardError && !allDetermined && undetermined.length > 0 && (
|
||||
<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>
|
||||
|
||||
{/* Vehicle list */}
|
||||
{selectedModel ? (
|
||||
filteredVehicles.length === 0 ? (
|
||||
<p className="py-4 text-center text-sm text-muted-foreground">
|
||||
{t("catalog.noModels")}
|
||||
{matchLoading ? (
|
||||
<div className="space-y-2">
|
||||
{Array.from({ length: 3 }).map((_, i) => (
|
||||
<Skeleton key={i} className="h-12 w-full rounded-lg" />
|
||||
))}
|
||||
</div>
|
||||
) : matchedVehicles && matchedVehicles.length > 0 ? (
|
||||
<div className="space-y-1">
|
||||
<p className="text-xs text-muted-foreground">
|
||||
{matchedVehicles.length} varyant
|
||||
</p>
|
||||
) : (
|
||||
<div className="space-y-1">
|
||||
<p className="text-xs text-muted-foreground">
|
||||
{filteredVehicles.length} varyant
|
||||
</p>
|
||||
{filteredVehicles.map((v) => (
|
||||
<VehicleRow
|
||||
key={v.id}
|
||||
vehicle={v}
|
||||
catalogCode={catalogCode}
|
||||
structured={structured}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
)
|
||||
{matchedVehicles.map((v) => (
|
||||
<VehicleRow
|
||||
key={v.id}
|
||||
vehicle={v}
|
||||
catalogCode={catalogCode}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
) : (
|
||||
<p className="py-8 text-center text-sm text-muted-foreground">
|
||||
{t("catalog.selectModel")}
|
||||
</p>
|
||||
<div className="rounded-lg border border-dashed border-border py-8 text-center">
|
||||
<Car className="mx-auto mb-2 size-8 text-muted-foreground/50" />
|
||||
<p className="text-sm text-muted-foreground">
|
||||
Bu araç konfigürasyonu için parça verisi henüz mevcut değil.
|
||||
</p>
|
||||
<p className="mt-1 text-xs text-muted-foreground/70">
|
||||
Farklı bir model veya varyant seçmeyi deneyin.
|
||||
</p>
|
||||
</div>
|
||||
)}
|
||||
</>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Empty state: no wizard rows */}
|
||||
{!wizardLoading &&
|
||||
!wizardError &&
|
||||
wizardRows &&
|
||||
wizardRows.length === 0 && (
|
||||
<p className="py-8 text-center text-muted-foreground">
|
||||
{t("catalog.noModels")}
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
// ── Reusable select ──────────────────────────────────
|
||||
// ── Wizard step: show options for first undetermined row ──
|
||||
|
||||
function FilterSelect({
|
||||
label,
|
||||
placeholder,
|
||||
value,
|
||||
options,
|
||||
onChange,
|
||||
function WizardStep({
|
||||
row,
|
||||
onSelect,
|
||||
}: {
|
||||
label: string;
|
||||
placeholder: string;
|
||||
value: string;
|
||||
options: string[];
|
||||
onChange: (val: string) => void;
|
||||
row: WizardRow;
|
||||
onSelect: (rowName: string, option: WizardOption) => void;
|
||||
}) {
|
||||
const [search, setSearch] = useState("");
|
||||
|
||||
const filtered = useMemo(() => {
|
||||
if (!search) return row.options;
|
||||
const q = search.toLowerCase();
|
||||
return row.options.filter((o) => o.value.toLowerCase().includes(q));
|
||||
}, [row.options, search]);
|
||||
|
||||
return (
|
||||
<div className="min-w-[160px] flex-1">
|
||||
<label className="mb-1 block text-xs font-medium text-muted-foreground">
|
||||
{label}
|
||||
</label>
|
||||
<select
|
||||
value={value}
|
||||
onChange={(e) => onChange(e.target.value)}
|
||||
className="h-9 w-full rounded-md border border-input bg-background px-3 text-sm shadow-sm focus:outline-none focus:ring-1 focus:ring-ring"
|
||||
>
|
||||
<option value="">{placeholder}</option>
|
||||
{options.map((o) => (
|
||||
<option key={o} value={o}>
|
||||
{o}
|
||||
</option>
|
||||
<div className="space-y-2">
|
||||
<div className="flex items-center justify-between">
|
||||
<h2 className="text-sm font-medium">{row.name}</h2>
|
||||
<span className="text-xs text-muted-foreground">
|
||||
{row.options.length} seçenek
|
||||
</span>
|
||||
</div>
|
||||
|
||||
{row.options.length > 8 && (
|
||||
<input
|
||||
type="text"
|
||||
value={search}
|
||||
onChange={(e) => setSearch(e.target.value)}
|
||||
placeholder="Ara..."
|
||||
className="h-8 w-full rounded-md border border-input bg-background px-3 text-sm shadow-sm focus:outline-none focus:ring-1 focus:ring-ring"
|
||||
/>
|
||||
)}
|
||||
|
||||
<div className="grid gap-1">
|
||||
{filtered.map((opt) => (
|
||||
<button
|
||||
key={opt.key}
|
||||
type="button"
|
||||
onClick={() => onSelect(row.name, opt)}
|
||||
className="flex items-center gap-2 rounded-lg border border-border bg-card px-3 py-2 text-left text-sm transition-colors hover:bg-accent"
|
||||
>
|
||||
<span className="min-w-0 flex-1 truncate">{opt.value}</span>
|
||||
<ChevronRight className="size-4 shrink-0 text-muted-foreground" />
|
||||
</button>
|
||||
))}
|
||||
</select>
|
||||
{filtered.length === 0 && (
|
||||
<p className="py-4 text-center text-sm text-muted-foreground">
|
||||
Sonuç bulunamadı
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -388,45 +334,28 @@ function FilterSelect({
|
||||
function VehicleRow({
|
||||
vehicle,
|
||||
catalogCode,
|
||||
structured,
|
||||
}: {
|
||||
vehicle: EmexVehicle;
|
||||
catalogCode: string;
|
||||
structured: boolean;
|
||||
}) {
|
||||
const parts: string[] = [];
|
||||
|
||||
if (structured) {
|
||||
const opts = parseOptions(vehicle.optionsRaw);
|
||||
if (opts["Engine type and suffix"])
|
||||
parts.push(opts["Engine type and suffix"]);
|
||||
else if (opts["Engine type"]) parts.push(opts["Engine type"]);
|
||||
if (opts["Gearbox type"]) parts.push(opts["Gearbox type"]);
|
||||
if (opts["Vehicle type"]) parts.push(opts["Vehicle type"]);
|
||||
} else {
|
||||
// Use optionsRaw as primary description when available — it's the most
|
||||
// informative string in most catalogs (trim, engine spec, market etc.)
|
||||
if (vehicle.optionsRaw) {
|
||||
parts.push(vehicle.optionsRaw);
|
||||
}
|
||||
// Add fields not already contained in optionsRaw
|
||||
const raw = vehicle.optionsRaw?.toLowerCase() || "";
|
||||
if (vehicle.engine && !raw.includes(vehicle.engine.toLowerCase()))
|
||||
parts.push(vehicle.engine);
|
||||
if (vehicle.bodyType && !raw.includes(vehicle.bodyType.toLowerCase()))
|
||||
parts.push(vehicle.bodyType);
|
||||
if (
|
||||
vehicle.transmission &&
|
||||
!raw.includes(vehicle.transmission.toLowerCase())
|
||||
)
|
||||
parts.push(vehicle.transmission);
|
||||
if (vehicle.driveType && !raw.includes(vehicle.driveType.toLowerCase()))
|
||||
parts.push(vehicle.driveType);
|
||||
if (vehicle.fuelType && !raw.includes(vehicle.fuelType.toLowerCase()))
|
||||
parts.push(vehicle.fuelType);
|
||||
if (vehicle.optionsRaw) {
|
||||
parts.push(vehicle.optionsRaw);
|
||||
}
|
||||
const raw = vehicle.optionsRaw?.toLowerCase() || "";
|
||||
if (vehicle.engine && !raw.includes(vehicle.engine.toLowerCase()))
|
||||
parts.push(vehicle.engine);
|
||||
if (vehicle.bodyType && !raw.includes(vehicle.bodyType.toLowerCase()))
|
||||
parts.push(vehicle.bodyType);
|
||||
if (
|
||||
vehicle.transmission &&
|
||||
!raw.includes(vehicle.transmission.toLowerCase())
|
||||
)
|
||||
parts.push(vehicle.transmission);
|
||||
if (vehicle.driveType && !raw.includes(vehicle.driveType.toLowerCase()))
|
||||
parts.push(vehicle.driveType);
|
||||
|
||||
// Fallback if nothing
|
||||
if (parts.length === 0 && vehicle.engine) parts.push(vehicle.engine);
|
||||
|
||||
return (
|
||||
|
||||
Reference in New Issue
Block a user