feat(pl24): make "bk. tablo:" BOM cross-references navigable
PL24 BOM emits "see table NNN-NNN" reference rows (oem N/A, target code in
remark) with NO upstream link. Resolve the code against the vehicle's
illustration index (codes live in category names as {NNN-NNN}) and render
jump links. Unresolved targets (branch not seeded yet) deep-link a pre-filled
catalog search via ?q=.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -1462,7 +1462,7 @@ export class CategoriesService {
|
||||
name: category.name,
|
||||
description: category.nameOriginal || null,
|
||||
parentId: category.parentId || null,
|
||||
parts: dbParts,
|
||||
parts: await this.resolvePartReferences(dbParts, category.vehicleId),
|
||||
schemaPics: mappedPics,
|
||||
hotspots: mappedHotspots,
|
||||
// Only meaningful when the lists are empty: true means a source fetch
|
||||
@@ -1471,6 +1471,51 @@ export class CategoriesService {
|
||||
};
|
||||
}
|
||||
|
||||
// PL24 BOM "bk. tablo:NNN-NNN" rows are cross-references to another
|
||||
// illustration, not real parts (oem_code "N/A", target code in `remark`).
|
||||
// Upstream gives us NO navigable link for them — only the text code — so we
|
||||
// resolve it ourselves: every illustration's code lives in its category name
|
||||
// as `{NNN-NNN}`, so we build a code→categoryId index for this vehicle and
|
||||
// attach it. Codes whose target branch hasn't been lazily seeded yet resolve
|
||||
// to null; the UI then falls back to a pre-filled catalog search.
|
||||
private static readonly REF_CODE_RE = /\d{3}-\d{3}/g;
|
||||
private static readonly REF_CODE_IN_NAME_RE = /\{(\d{3}-\d{3})\}/g;
|
||||
|
||||
private async resolvePartReferences<T extends { name: string; remark: string | null }>(
|
||||
rows: T[],
|
||||
vehicleId: string | null,
|
||||
): Promise<(T & { references?: Array<{ code: string; categoryId: string | null }> })[]> {
|
||||
const isRef = (p: T) => p.name?.includes("bk. tablo");
|
||||
if (!vehicleId || !rows.some(isRef)) return rows;
|
||||
|
||||
const cats = await this.db
|
||||
.select({ id: categories.id, name: categories.name })
|
||||
.from(categories)
|
||||
.where(eq(categories.vehicleId, vehicleId));
|
||||
|
||||
const index = new Map<string, string>();
|
||||
for (const c of cats) {
|
||||
for (const m of c.name.matchAll(CategoriesService.REF_CODE_IN_NAME_RE)) {
|
||||
if (!index.has(m[1])) index.set(m[1], c.id);
|
||||
}
|
||||
}
|
||||
|
||||
return rows.map((p) => {
|
||||
if (!isRef(p)) return p;
|
||||
// remark stores the code possibly escaped ("819\-031") and sometimes
|
||||
// multiple codes concatenated ("141-045141-065141-075") plus position
|
||||
// hints ("803-070 POS.28+29") — extract every NNN-NNN occurrence.
|
||||
const codes = [
|
||||
...new Set((p.remark?.replace(/\\/g, "") ?? "").match(CategoriesService.REF_CODE_RE) ?? []),
|
||||
];
|
||||
if (codes.length === 0) return p;
|
||||
return {
|
||||
...p,
|
||||
references: codes.map((code) => ({ code, categoryId: index.get(code) ?? null })),
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
async getById(categoryId: string) {
|
||||
const [category] = await this.db
|
||||
.select()
|
||||
|
||||
Reference in New Issue
Block a user