feat(p): reverse catalog on OEM detail — your vehicles using this code
New "Bu kod kataloğunuzda" section lists the user's decoded vehicles whose parts include the queried OEM code, each linking to the schema page that shows it. Pure sase data (parts.oem_code → vehicles, indexed exact match) — no TecDoc / vehicle-structure dependency. GET /parts/oem-vehicles?code=. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -15,6 +15,13 @@ export class PartsController {
|
||||
return this.partsService.searchByOem(oem);
|
||||
}
|
||||
|
||||
// Reverse catalog for the OEM detail page: which of the user's vehicles use
|
||||
// this OEM code. Declared before ":id" so the literal path wins the match.
|
||||
@Get("oem-vehicles")
|
||||
async vehiclesByOem(@Query("code") code: string) {
|
||||
return this.partsService.vehiclesByOem(code ?? "");
|
||||
}
|
||||
|
||||
@Get(":id")
|
||||
async getById(@Param("id") id: string) {
|
||||
return this.partsService.getById(id);
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { Inject, Injectable, Logger, NotFoundException } from "@nestjs/common";
|
||||
import { eq, like } from "drizzle-orm";
|
||||
import { eq, like, sql } from "drizzle-orm";
|
||||
import { DATABASE, type Database } from "../database/database.provider";
|
||||
import { categories, parts, schemaPics, vehicles } from "../database/schema/core";
|
||||
import { PL24Service } from "../integrations/pl24/pl24.service";
|
||||
@@ -142,4 +142,32 @@ export class PartsService {
|
||||
if (!part) throw new NotFoundException("Parça bulunamadı");
|
||||
return part;
|
||||
}
|
||||
|
||||
/**
|
||||
* Reverse catalog: the user's own decoded vehicles whose parts list contains
|
||||
* this exact OEM code. Powers the "bu kod kataloğunuzda şu araçlarda var"
|
||||
* section of the OEM detail page — pure sase data, no TecDoc/vehicle-structure
|
||||
* dependency. Exact match on the indexed `oem_code` (the code came from a real
|
||||
* part row, so the spelling matches). One representative `categoryId` per
|
||||
* vehicle lets the UI deep-link straight to a schema page showing the part.
|
||||
*/
|
||||
async vehiclesByOem(oemCode: string) {
|
||||
const code = (oemCode ?? "").trim();
|
||||
if (!code) return [];
|
||||
return this.db
|
||||
.select({
|
||||
vehicleId: vehicles.id,
|
||||
brandName: vehicles.brandName,
|
||||
model: vehicles.model,
|
||||
year: vehicles.year,
|
||||
categoryId: sql<string>`(array_agg(${parts.categoryId} ORDER BY ${parts.categoryId}))[1]`,
|
||||
occurrences: sql<number>`count(*)::int`,
|
||||
})
|
||||
.from(parts)
|
||||
.innerJoin(vehicles, eq(parts.vehicleId, vehicles.id))
|
||||
.where(eq(parts.oemCode, code))
|
||||
.groupBy(vehicles.id, vehicles.brandName, vehicles.model, vehicles.year)
|
||||
.orderBy(vehicles.brandName, vehicles.model)
|
||||
.limit(50);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -7,6 +7,7 @@ import {
|
||||
AlertCircle,
|
||||
ArrowLeft,
|
||||
Check,
|
||||
ChevronRight,
|
||||
Copy,
|
||||
ImageOff,
|
||||
PackageSearch,
|
||||
@@ -39,6 +40,17 @@ interface POemResult {
|
||||
truncated: boolean;
|
||||
}
|
||||
|
||||
/** A decoded vehicle in the user's own catalog whose parts include this OEM
|
||||
* code (reverse catalog — sase data, independent of the P snapshot). */
|
||||
interface CatalogVehicle {
|
||||
vehicleId: string;
|
||||
brandName: string | null;
|
||||
model: string | null;
|
||||
year: number | null;
|
||||
categoryId: string;
|
||||
occurrences: number;
|
||||
}
|
||||
|
||||
/** Normalise a code for search: strip non-alphanumerics + uppercase, so a query
|
||||
* like "1j0 973" matches a stored "1J0 973 702". */
|
||||
const normCode = (s: string) => s.toUpperCase().replace(/[^A-Z0-9]/g, "");
|
||||
@@ -113,6 +125,13 @@ function OemDetailPage() {
|
||||
queryFn: () => api.get<POemResult>(`/p/oem?code=${encodeURIComponent(code)}`),
|
||||
});
|
||||
|
||||
// Reverse catalog: the user's own vehicles that use this OEM code (sase data).
|
||||
const { data: catalogVehicles } = useQuery({
|
||||
queryKey: ["oem-vehicles", code],
|
||||
queryFn: () =>
|
||||
api.get<CatalogVehicle[]>(`/parts/oem-vehicles?code=${encodeURIComponent(code)}`),
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
if (data) {
|
||||
capture("oem_detail_viewed", {
|
||||
@@ -319,6 +338,36 @@ function OemDetailPage() {
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* ─── Reverse catalog: your vehicles that use this code ───────────── */}
|
||||
{catalogVehicles && catalogVehicles.length > 0 && (
|
||||
<section className="space-y-3">
|
||||
<div>
|
||||
<h2 className="text-sm font-semibold">Bu kod kataloğunuzda</h2>
|
||||
<p className="mt-0.5 text-xs text-muted-foreground">
|
||||
Bu OEM kodunun geçtiği araçlarınız
|
||||
</p>
|
||||
</div>
|
||||
<div className="grid gap-2 sm:grid-cols-2">
|
||||
{catalogVehicles.map((v) => (
|
||||
<Link
|
||||
key={v.vehicleId}
|
||||
to="/dashboard/vehicles/$id/categories/$categoryId"
|
||||
params={{ id: v.vehicleId, categoryId: v.categoryId }}
|
||||
className="flex items-center justify-between gap-2 rounded-xl border border-border bg-background p-3 transition-colors hover:bg-accent"
|
||||
>
|
||||
<span className="min-w-0">
|
||||
<span className="block truncate text-sm font-medium">
|
||||
{[v.brandName, v.model].filter(Boolean).join(" ") || "Araç"}
|
||||
</span>
|
||||
{v.year && <span className="text-xs text-muted-foreground">{v.year}</span>}
|
||||
</span>
|
||||
<ChevronRight className="size-4 shrink-0 text-muted-foreground" />
|
||||
</Link>
|
||||
))}
|
||||
</div>
|
||||
</section>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user