forked from Yara724/api
701 lines
22 KiB
TypeScript
701 lines
22 KiB
TypeScript
import {
|
|
ClaimVehicleTypeV2,
|
|
OUTER_PARTS_BY_CAR_TYPE,
|
|
OuterPartCatalogItem,
|
|
OuterPartSideV2,
|
|
} from "src/static/outer-car-parts-catalog";
|
|
|
|
/** Stored on `damage.selectedParts` and used when resolving captures / APIs. */
|
|
export type DamageSelectedPartV2 = {
|
|
id: number | null;
|
|
name: string;
|
|
side: string;
|
|
label_fa: string;
|
|
/** Original catalog key when known (e.g. `left_backfender`). */
|
|
catalogKey?: string;
|
|
};
|
|
|
|
const SIDE_SET = new Set<string>(Object.values(OuterPartSideV2));
|
|
|
|
/** Farsi side suffix for disambiguated labels, e.g. `گلگیر عقب (چپ)`. */
|
|
const SIDE_LABEL_FA: Record<string, string> = {
|
|
[OuterPartSideV2.LEFT]: "چپ",
|
|
[OuterPartSideV2.RIGHT]: "راست",
|
|
[OuterPartSideV2.FRONT]: "جلو",
|
|
[OuterPartSideV2.BACK]: "عقب",
|
|
[OuterPartSideV2.TOP]: "بالا",
|
|
};
|
|
|
|
function outerCatalogListForItem(item: OuterPartCatalogItem): OuterPartCatalogItem[] {
|
|
for (const list of Object.values(OUTER_PARTS_BY_CAR_TYPE)) {
|
|
if (list.some((x) => x.key === item.key || x.id === item.id)) {
|
|
return list;
|
|
}
|
|
}
|
|
return [item];
|
|
}
|
|
|
|
/**
|
|
* When multiple catalog rows share the same `titleFa` (e.g. left/right rear fender),
|
|
* append ` (side)` in Farsi so clients can display `label_fa` alone.
|
|
*/
|
|
export function disambiguateOuterPartLabelFa(
|
|
baseTitleFa: string,
|
|
side: string,
|
|
sameTypeCatalog: OuterPartCatalogItem[],
|
|
): string {
|
|
const t = String(baseTitleFa ?? "").trim();
|
|
const s = String(side ?? "").trim();
|
|
if (!t || !sameTypeCatalog?.length) return t;
|
|
const norm = (x: string) => x.trim();
|
|
const siblings = sameTypeCatalog.filter((p) => norm(p.titleFa) === norm(t));
|
|
if (siblings.length <= 1) return t;
|
|
const sideFa = SIDE_LABEL_FA[s] || s;
|
|
if (!sideFa) return t;
|
|
return `${t} (${sideFa})`;
|
|
}
|
|
|
|
const CATALOG_ITEM_BY_ID = new Map<number, OuterPartCatalogItem>();
|
|
for (const list of Object.values(OUTER_PARTS_BY_CAR_TYPE)) {
|
|
for (const it of list) {
|
|
CATALOG_ITEM_BY_ID.set(it.id, it);
|
|
}
|
|
}
|
|
|
|
function findCatalogItemByKey(key: string): OuterPartCatalogItem | undefined {
|
|
for (const list of Object.values(OUTER_PARTS_BY_CAR_TYPE)) {
|
|
const hit = list.find((x) => x.key === key);
|
|
if (hit) return hit;
|
|
}
|
|
return undefined;
|
|
}
|
|
|
|
/** Strip leading `left_|right_|…` when present so `name` is side-agnostic for clients. */
|
|
export function splitCatalogKeyToNameAndSide(
|
|
fullKey: string,
|
|
): { name: string; side: string } {
|
|
const t = String(fullKey ?? "").trim();
|
|
const i = t.indexOf("_");
|
|
if (i <= 0) return { name: t, side: "" };
|
|
const head = t.slice(0, i);
|
|
if (SIDE_SET.has(head)) {
|
|
return { side: head, name: t.slice(i + 1) };
|
|
}
|
|
return { name: t, side: "" };
|
|
}
|
|
|
|
export function catalogLikeKeyFromPart(
|
|
part: Pick<DamageSelectedPartV2, "side" | "name">,
|
|
): string {
|
|
if (part.side) return `${part.side}_${part.name}`;
|
|
return part.name;
|
|
}
|
|
|
|
export function catalogItemToSelectedPart(
|
|
item: OuterPartCatalogItem,
|
|
sameTypeCatalog?: OuterPartCatalogItem[],
|
|
): DamageSelectedPartV2 {
|
|
const { name, side } = splitCatalogKeyToNameAndSide(item.key);
|
|
const sideEff = String(side || item.side || "");
|
|
const list = sameTypeCatalog ?? outerCatalogListForItem(item);
|
|
const label_fa = disambiguateOuterPartLabelFa(item.titleFa, sideEff, list);
|
|
return {
|
|
id: item.id,
|
|
name,
|
|
side: sideEff,
|
|
label_fa,
|
|
catalogKey: item.key,
|
|
};
|
|
}
|
|
|
|
const LEGACY_SLUG_LABEL_FA: Record<string, string> = {
|
|
hood: "کاپوت",
|
|
trunk: "صندوق عقب",
|
|
roof: "سقف",
|
|
front_right_door: "درب جلو راست",
|
|
front_left_door: "درب جلو چپ",
|
|
rear_right_door: "درب عقب راست",
|
|
rear_left_door: "درب عقب چپ",
|
|
front_bumper: "سپر جلو",
|
|
rear_bumper: "سپر عقب",
|
|
front_right_fender: "گلگیر جلو راست",
|
|
front_left_fender: "گلگیر جلو چپ",
|
|
rear_right_fender: "گلگیر عقب راست",
|
|
rear_left_fender: "گلگیر عقب چپ",
|
|
};
|
|
|
|
function labelFaForUnknownSlug(slug: string): string {
|
|
return LEGACY_SLUG_LABEL_FA[slug] || slug;
|
|
}
|
|
|
|
function toNum(v: unknown): number | null {
|
|
if (v == null || v === "") return null;
|
|
const n = Number(v);
|
|
return Number.isFinite(n) ? n : null;
|
|
}
|
|
|
|
/**
|
|
* Normalize `damage.selectedParts` from DB (strings, legacy objects, or new objects)
|
|
* into a stable list for APIs and capture resolution.
|
|
*/
|
|
export function normalizeDamageSelectedParts(
|
|
raw: unknown,
|
|
carType?: ClaimVehicleTypeV2,
|
|
selectedOuterPartsLegacy?: unknown,
|
|
): DamageSelectedPartV2[] {
|
|
const arr = Array.isArray(raw) ? raw : [];
|
|
const legacyOuter = Array.isArray(selectedOuterPartsLegacy)
|
|
? selectedOuterPartsLegacy
|
|
: [];
|
|
const byKeyFromLegacy = new Map<string, { id?: number; side?: string }>();
|
|
for (const o of legacyOuter) {
|
|
if (o && typeof o === "object" && "key" in (o as object)) {
|
|
const rec = o as { key?: string; id?: number; side?: string };
|
|
const k = String(rec.key || "").trim();
|
|
if (k) byKeyFromLegacy.set(k, { id: rec.id, side: rec.side });
|
|
}
|
|
}
|
|
const catalog = carType ? OUTER_PARTS_BY_CAR_TYPE[carType] : undefined;
|
|
const byCatalogKey = catalog
|
|
? new Map(catalog.map((c) => [c.key, c]))
|
|
: undefined;
|
|
|
|
const out: DamageSelectedPartV2[] = [];
|
|
for (const el of arr) {
|
|
if (el == null) continue;
|
|
if (typeof el === "string") {
|
|
const s = el.trim();
|
|
if (!s) continue;
|
|
let catItem = byCatalogKey?.get(s) ?? findCatalogItemByKey(s);
|
|
if (!catItem && /^\d+$/.test(s)) {
|
|
catItem = CATALOG_ITEM_BY_ID.get(Number(s));
|
|
}
|
|
if (catItem) {
|
|
const list = catalog ?? outerCatalogListForItem(catItem);
|
|
out.push(catalogItemToSelectedPart(catItem, list));
|
|
continue;
|
|
}
|
|
const hint = byKeyFromLegacy.get(s);
|
|
if (hint?.side || hint?.id != null) {
|
|
const split = splitCatalogKeyToNameAndSide(s);
|
|
if (split.side) {
|
|
const sideEff = hint?.side ?? split.side;
|
|
const baseFa = labelFaForUnknownSlug(s);
|
|
const list = catalog ?? [];
|
|
const label_fa =
|
|
catalog?.length && baseFa
|
|
? disambiguateOuterPartLabelFa(baseFa, String(sideEff), catalog)
|
|
: baseFa;
|
|
out.push({
|
|
id: hint?.id ?? null,
|
|
name: split.name,
|
|
side: sideEff,
|
|
label_fa,
|
|
catalogKey: s,
|
|
});
|
|
continue;
|
|
}
|
|
}
|
|
out.push({
|
|
id: hint?.id ?? null,
|
|
name: s,
|
|
side: hint?.side ?? "",
|
|
label_fa: labelFaForUnknownSlug(s),
|
|
catalogKey: s,
|
|
});
|
|
continue;
|
|
}
|
|
if (typeof el === "object") {
|
|
const o = el as Record<string, unknown>;
|
|
if (typeof o.name === "string" && o.name.trim()) {
|
|
let name = o.name.trim();
|
|
const inner = splitCatalogKeyToNameAndSide(name);
|
|
if (inner.side && inner.name) {
|
|
name = inner.name;
|
|
}
|
|
const side = typeof o.side === "string" ? o.side : inner.side || "";
|
|
const id = toNum(o.id);
|
|
const ck =
|
|
typeof o.catalogKey === "string" && o.catalogKey.trim()
|
|
? o.catalogKey.trim()
|
|
: undefined;
|
|
let catItem: OuterPartCatalogItem | undefined;
|
|
if (catalog && byCatalogKey) {
|
|
if (ck) catItem = byCatalogKey.get(ck);
|
|
if (!catItem && id != null) catItem = catalog.find((c) => c.id === id);
|
|
if (!catItem && side && name) {
|
|
catItem = byCatalogKey.get(catalogLikeKeyFromPart({ side, name }));
|
|
}
|
|
} else {
|
|
if (ck) catItem = findCatalogItemByKey(ck);
|
|
if (!catItem && id != null) {
|
|
catItem = CATALOG_ITEM_BY_ID.get(id!) ?? undefined;
|
|
}
|
|
if (!catItem && side && name) {
|
|
catItem = findCatalogItemByKey(
|
|
catalogLikeKeyFromPart({ side, name }),
|
|
);
|
|
}
|
|
}
|
|
const fallbackLabel =
|
|
typeof o.label_fa === "string" && o.label_fa.trim()
|
|
? o.label_fa.trim()
|
|
: typeof o.labelFa === "string" && o.labelFa.trim()
|
|
? (o.labelFa as string).trim()
|
|
: name;
|
|
const listForDis = catalog?.length
|
|
? catalog
|
|
: catItem
|
|
? outerCatalogListForItem(catItem)
|
|
: [];
|
|
const label_fa =
|
|
catItem && listForDis.length
|
|
? disambiguateOuterPartLabelFa(
|
|
catItem.titleFa,
|
|
catItem.side,
|
|
listForDis,
|
|
)
|
|
: fallbackLabel;
|
|
out.push({
|
|
id,
|
|
name,
|
|
side,
|
|
label_fa,
|
|
catalogKey: ck,
|
|
});
|
|
continue;
|
|
}
|
|
if (typeof o.key === "string" && o.key.trim()) {
|
|
const k = o.key.trim();
|
|
const cat =
|
|
byCatalogKey?.get(k) ??
|
|
findCatalogItemByKey(k) ??
|
|
(toNum(o.id) != null ? CATALOG_ITEM_BY_ID.get(toNum(o.id)!) : undefined);
|
|
if (cat) {
|
|
const list = catalog ?? outerCatalogListForItem(cat);
|
|
out.push(catalogItemToSelectedPart(cat, list));
|
|
continue;
|
|
}
|
|
const split = splitCatalogKeyToNameAndSide(k);
|
|
out.push({
|
|
id: toNum(o.id),
|
|
name: split.side ? split.name : k,
|
|
side: typeof o.side === "string" ? o.side : split.side,
|
|
label_fa:
|
|
typeof o.label_fa === "string" && o.label_fa.trim()
|
|
? (o.label_fa as string).trim()
|
|
: labelFaForUnknownSlug(k),
|
|
catalogKey: k,
|
|
});
|
|
}
|
|
}
|
|
}
|
|
return out;
|
|
}
|
|
|
|
export function partIdentityKey(p: DamageSelectedPartV2): string {
|
|
if (p.id != null && Number.isFinite(p.id)) return `id:${p.id}`;
|
|
return `${p.side}|${p.name}`;
|
|
}
|
|
|
|
function normPartSegment(s: string): string {
|
|
return String(s ?? "").replace(/[^a-z0-9]/gi, "").toLowerCase();
|
|
}
|
|
|
|
function looseCatalogNameMatch(
|
|
catalogItem: OuterPartCatalogItem,
|
|
expertPart: string,
|
|
): boolean {
|
|
const { name } = splitCatalogKeyToNameAndSide(catalogItem.key);
|
|
return normPartSegment(name) === normPartSegment(expertPart);
|
|
}
|
|
|
|
/**
|
|
* Map legacy expert reply `{ part: "backFender", side: "left" }` to a catalog row
|
|
* when `carType` is known (or scan all types if omitted).
|
|
*/
|
|
export function findCatalogItemFromLegacyExpertPart(
|
|
part: string,
|
|
side: string,
|
|
carType?: ClaimVehicleTypeV2,
|
|
): OuterPartCatalogItem | undefined {
|
|
const s = String(side ?? "").toLowerCase().trim();
|
|
const lists = carType
|
|
? [OUTER_PARTS_BY_CAR_TYPE[carType]].filter(Boolean) as OuterPartCatalogItem[][]
|
|
: (Object.values(OUTER_PARTS_BY_CAR_TYPE) as OuterPartCatalogItem[][]);
|
|
for (const catalog of lists) {
|
|
if (!catalog?.length) continue;
|
|
for (const it of catalog) {
|
|
if (String(it.side).toLowerCase() !== s) continue;
|
|
if (looseCatalogNameMatch(it, part)) return it;
|
|
}
|
|
}
|
|
return undefined;
|
|
}
|
|
|
|
function selectedPartToStoredRecord(p: DamageSelectedPartV2): Record<string, unknown> {
|
|
const r: Record<string, unknown> = {
|
|
name: p.name,
|
|
side: String(p.side || "").toLowerCase(),
|
|
label_fa: p.label_fa,
|
|
};
|
|
if (p.id != null && Number.isFinite(p.id)) r.id = p.id;
|
|
if (p.catalogKey) r.catalogKey = p.catalogKey;
|
|
return r;
|
|
}
|
|
|
|
/**
|
|
* Canonical shape for `evaluation.damageExpertReply*.parts[].carPartDamage`
|
|
* (same fields as owner `damage.selectedParts` items).
|
|
*/
|
|
export function normalizeCarPartDamageForExpertReply(
|
|
raw: unknown,
|
|
carType?: ClaimVehicleTypeV2,
|
|
): Record<string, unknown> {
|
|
if (raw == null) {
|
|
throw new Error("carPartDamage is required");
|
|
}
|
|
if (typeof raw === "string") {
|
|
const s = raw.trim();
|
|
if (!s) throw new Error("carPartDamage is empty");
|
|
const fromKey = findCatalogItemByKey(s);
|
|
if (fromKey) {
|
|
const list = outerCatalogListForItem(fromKey);
|
|
return selectedPartToStoredRecord(
|
|
catalogItemToSelectedPart(fromKey, list),
|
|
);
|
|
}
|
|
return { name: s, side: "", label_fa: s };
|
|
}
|
|
if (typeof raw !== "object" || raw === null || Array.isArray(raw)) {
|
|
throw new Error("Invalid carPartDamage");
|
|
}
|
|
const o = raw as Record<string, unknown>;
|
|
const legacyPart =
|
|
typeof o.part === "string" && o.part.trim() ? o.part.trim() : "";
|
|
const legacySide =
|
|
typeof o.side === "string" && o.side.trim()
|
|
? o.side.trim().toLowerCase()
|
|
: "";
|
|
const name = typeof o.name === "string" && o.name.trim() ? o.name.trim() : "";
|
|
const sideIn =
|
|
typeof o.side === "string" && o.side.trim()
|
|
? o.side.trim().toLowerCase()
|
|
: "";
|
|
|
|
if (name && sideIn) {
|
|
const id = toNum(o.id);
|
|
const ck =
|
|
typeof o.catalogKey === "string" && o.catalogKey.trim()
|
|
? o.catalogKey.trim()
|
|
: undefined;
|
|
const catalog = carType ? OUTER_PARTS_BY_CAR_TYPE[carType] : undefined;
|
|
let catItem: OuterPartCatalogItem | undefined;
|
|
if (catalog) {
|
|
const byKey = new Map(catalog.map((c) => [c.key, c]));
|
|
if (ck) catItem = byKey.get(ck);
|
|
if (!catItem && id != null) catItem = catalog.find((c) => c.id === id);
|
|
if (!catItem) {
|
|
catItem = byKey.get(catalogLikeKeyFromPart({ side: sideIn, name }));
|
|
}
|
|
}
|
|
if (!catItem && id != null) {
|
|
catItem = CATALOG_ITEM_BY_ID.get(id!) ?? undefined;
|
|
}
|
|
const list = catalog?.length
|
|
? catalog
|
|
: catItem
|
|
? outerCatalogListForItem(catItem)
|
|
: [];
|
|
if (catItem && list.length) {
|
|
return selectedPartToStoredRecord(
|
|
catalogItemToSelectedPart(catItem, list),
|
|
);
|
|
}
|
|
const label_fa =
|
|
typeof o.label_fa === "string" && o.label_fa.trim()
|
|
? o.label_fa.trim()
|
|
: name;
|
|
const out: Record<string, unknown> = {
|
|
name,
|
|
side: sideIn,
|
|
label_fa,
|
|
};
|
|
if (id != null) out.id = id;
|
|
if (ck) out.catalogKey = ck;
|
|
return out;
|
|
}
|
|
|
|
if (legacyPart && legacySide) {
|
|
const catItem = findCatalogItemFromLegacyExpertPart(
|
|
legacyPart,
|
|
legacySide,
|
|
carType,
|
|
);
|
|
if (catItem) {
|
|
const list = outerCatalogListForItem(catItem);
|
|
return selectedPartToStoredRecord(
|
|
catalogItemToSelectedPart(catItem, list),
|
|
);
|
|
}
|
|
const sideFa = SIDE_LABEL_FA[legacySide] || legacySide;
|
|
return {
|
|
name: legacyPart,
|
|
side: legacySide,
|
|
label_fa: `${legacyPart} (${sideFa})`,
|
|
};
|
|
}
|
|
|
|
throw new Error("carPartDamage must include side and (name or part)");
|
|
}
|
|
|
|
/** Best-effort migration for API output; never throws. */
|
|
export function migrateExpertReplyCarPartDamageToUnified(
|
|
raw: unknown,
|
|
carType?: ClaimVehicleTypeV2,
|
|
): Record<string, unknown> {
|
|
try {
|
|
return normalizeCarPartDamageForExpertReply(raw, carType);
|
|
} catch {
|
|
if (raw && typeof raw === "object" && !Array.isArray(raw)) {
|
|
return { ...(raw as Record<string, unknown>) };
|
|
}
|
|
return { name: String(raw ?? ""), side: "", label_fa: String(raw ?? "") };
|
|
}
|
|
}
|
|
|
|
/** Stable string for factor DB rows / logs from unified or legacy `carPartDamage`. */
|
|
export function carPartDamageLabelForFactorRecord(cd: unknown): string {
|
|
if (cd == null) return "";
|
|
if (typeof cd === "string") return cd.trim();
|
|
if (typeof cd === "object") {
|
|
const o = cd as Record<string, unknown>;
|
|
if (typeof o.label_fa === "string" && o.label_fa.trim()) {
|
|
return o.label_fa.trim();
|
|
}
|
|
if (typeof o.part === "string" && o.part.trim()) {
|
|
const side = typeof o.side === "string" ? o.side.trim() : "";
|
|
const sideFa = side ? SIDE_LABEL_FA[side.toLowerCase()] || side : "";
|
|
return sideFa ? `${o.part.trim()} (${sideFa})` : o.part.trim();
|
|
}
|
|
if (typeof o.name === "string" && o.name.trim()) {
|
|
const side = typeof o.side === "string" ? o.side.trim() : "";
|
|
const sideFa = side ? SIDE_LABEL_FA[side.toLowerCase()] || side : "";
|
|
return sideFa ? `${o.name.trim()} (${sideFa})` : o.name.trim();
|
|
}
|
|
}
|
|
return String(cd);
|
|
}
|
|
|
|
export function mediaRowHasCapture(row: unknown): boolean {
|
|
if (!row || typeof row !== "object") return false;
|
|
const r = row as { path?: string; url?: string };
|
|
return !!(r.path || r.url);
|
|
}
|
|
|
|
/** Whether `media.damagedParts` is the new array shape (possibly empty). */
|
|
export function isDamagedPartsMediaArray(data: unknown): boolean {
|
|
return Array.isArray(data);
|
|
}
|
|
|
|
function getLegacyMapRecord(
|
|
data: unknown,
|
|
): Record<string, unknown> | null {
|
|
if (data == null) return null;
|
|
if (data instanceof Map) {
|
|
return Object.fromEntries((data as Map<string, unknown>).entries());
|
|
}
|
|
if (typeof data === "object" && !Array.isArray(data)) {
|
|
return data as Record<string, unknown>;
|
|
}
|
|
return null;
|
|
}
|
|
|
|
function getCaptureBlobLoose(
|
|
legacyMap: Record<string, unknown>,
|
|
partKey: string,
|
|
): unknown {
|
|
const pk = String(partKey ?? "").trim();
|
|
if (!pk) return undefined;
|
|
let b = legacyMap[pk];
|
|
if (b) return b;
|
|
const target = pk.toLowerCase().replace(/[\s_-]/g, "");
|
|
for (const k of Object.keys(legacyMap)) {
|
|
if (k.toLowerCase().replace(/[\s_-]/g, "") === target) {
|
|
return legacyMap[k];
|
|
}
|
|
}
|
|
return undefined;
|
|
}
|
|
|
|
/**
|
|
* Align legacy `media.damagedParts` map with `selected` order → array rows
|
|
* (metadata + any existing capture blobs).
|
|
*/
|
|
export function migrateLegacyDamagedPartsMapToArray(
|
|
legacyMap: Record<string, unknown>,
|
|
selected: DamageSelectedPartV2[],
|
|
): Record<string, unknown>[] {
|
|
return selected.map((sp) => {
|
|
const keysTry = [
|
|
sp.catalogKey,
|
|
catalogLikeKeyFromPart(sp),
|
|
sp.name,
|
|
].filter(Boolean) as string[];
|
|
let blob: unknown;
|
|
for (const k of keysTry) {
|
|
blob = getCaptureBlobLoose(legacyMap, k);
|
|
if (blob) break;
|
|
}
|
|
const base = {
|
|
id: sp.id ?? undefined,
|
|
name: sp.name,
|
|
side: sp.side,
|
|
label_fa: sp.label_fa,
|
|
};
|
|
if (blob && typeof blob === "object") {
|
|
const c = blob as Record<string, unknown>;
|
|
return {
|
|
...base,
|
|
...(c.path ? { path: c.path } : {}),
|
|
...(c.fileName ? { fileName: c.fileName } : {}),
|
|
...(c.url ? { url: c.url } : {}),
|
|
...(c.capturedAt ? { capturedAt: c.capturedAt } : {}),
|
|
};
|
|
}
|
|
return { ...base };
|
|
});
|
|
}
|
|
|
|
/**
|
|
* Always returns an array view: migrates legacy map → array when needed.
|
|
*/
|
|
export function coerceDamagedPartsMediaToArray(
|
|
damagedPartsData: unknown,
|
|
selected: DamageSelectedPartV2[],
|
|
): Record<string, unknown>[] {
|
|
if (Array.isArray(damagedPartsData)) {
|
|
const arr = damagedPartsData.map((x) =>
|
|
typeof x === "object" && x ? { ...(x as object) } : {},
|
|
) as Record<string, unknown>[];
|
|
while (arr.length < selected.length) {
|
|
arr.push({});
|
|
}
|
|
return arr;
|
|
}
|
|
const legacy = getLegacyMapRecord(damagedPartsData);
|
|
if (legacy && selected.length) {
|
|
return migrateLegacyDamagedPartsMapToArray(legacy, selected);
|
|
}
|
|
return [];
|
|
}
|
|
|
|
/**
|
|
* Resolve `captureKey` to an index into `selected` / `media.damagedParts`.
|
|
*
|
|
* Supported keys: catalog id (number string), array index when unambiguous,
|
|
* full catalog key (`left_backfender`), or `side_name` matching stored part.
|
|
*/
|
|
export function resolvePartCaptureIndex(
|
|
captureKeyRaw: string,
|
|
selected: DamageSelectedPartV2[],
|
|
damagedPartsArray?: Record<string, unknown>[],
|
|
): number {
|
|
const raw = String(captureKeyRaw ?? "").trim();
|
|
if (!raw) return -1;
|
|
|
|
if (selected.length) {
|
|
for (let i = 0; i < selected.length; i++) {
|
|
const sp = selected[i];
|
|
if (sp.catalogKey === raw) return i;
|
|
if (catalogLikeKeyFromPart(sp) === raw) return i;
|
|
}
|
|
|
|
if (/^\d+$/.test(raw)) {
|
|
const n = Number(raw);
|
|
const byId = selected.findIndex((p) => p.id === n);
|
|
if (byId >= 0) return byId;
|
|
const hasIdCollision = selected.some((p) => p.id === n);
|
|
if (!hasIdCollision && n >= 0 && n < selected.length) return n;
|
|
}
|
|
|
|
const loose = raw.toLowerCase().replace(/[\s_-]/g, "");
|
|
for (let i = 0; i < selected.length; i++) {
|
|
const sp = selected[i];
|
|
const cand = [
|
|
sp.catalogKey,
|
|
catalogLikeKeyFromPart(sp),
|
|
sp.name,
|
|
].filter(Boolean) as string[];
|
|
for (const c of cand) {
|
|
if (c.toLowerCase().replace(/[\s_-]/g, "") === loose) return i;
|
|
}
|
|
}
|
|
}
|
|
|
|
if (damagedPartsArray && damagedPartsArray.length) {
|
|
for (let i = 0; i < damagedPartsArray.length; i++) {
|
|
const row = damagedPartsArray[i];
|
|
if (!row || typeof row !== "object") continue;
|
|
const id = toNum((row as { id?: unknown }).id);
|
|
if (id != null && String(id) === raw) return i;
|
|
const ck = String((row as { catalogKey?: unknown }).catalogKey || "");
|
|
if (ck && ck === raw) return i;
|
|
const name = String((row as { name?: unknown }).name || "");
|
|
const side = String((row as { side?: unknown }).side || "");
|
|
if (side && name && `${side}_${name}` === raw) return i;
|
|
}
|
|
}
|
|
|
|
return -1;
|
|
}
|
|
|
|
export function mediaRowOrLegacyHasCapture(
|
|
damagedPartsData: unknown,
|
|
partKey: string,
|
|
selected: DamageSelectedPartV2[],
|
|
): boolean {
|
|
if (damagedPartsData == null) return false;
|
|
if (Array.isArray(damagedPartsData)) {
|
|
const idx = resolvePartCaptureIndex(partKey, selected, damagedPartsData as any[]);
|
|
if (idx < 0) return false;
|
|
return mediaRowHasCapture((damagedPartsData as unknown[])[idx]);
|
|
}
|
|
const legacy = getLegacyMapRecord(damagedPartsData);
|
|
if (!legacy) return false;
|
|
return !!getCaptureBlobLoose(legacy, partKey);
|
|
}
|
|
|
|
export function selectedPartsLegacyStringsForAngles(
|
|
selected: DamageSelectedPartV2[],
|
|
): string[] {
|
|
const s = new Set<string>();
|
|
for (const p of selected) {
|
|
if (p.catalogKey) s.add(p.catalogKey);
|
|
s.add(catalogLikeKeyFromPart(p));
|
|
if (p.name) s.add(p.name);
|
|
}
|
|
return [...s];
|
|
}
|
|
|
|
/** String keys that might appear under legacy `media.damagedParts` for this selection. */
|
|
export function damageSelectedPartsToLookupStrings(
|
|
raw: unknown,
|
|
carType?: ClaimVehicleTypeV2,
|
|
selectedOuterPartsLegacy?: unknown,
|
|
): string[] {
|
|
const norm = normalizeDamageSelectedParts(
|
|
raw,
|
|
carType,
|
|
selectedOuterPartsLegacy,
|
|
);
|
|
if (norm.length) return selectedPartsLegacyStringsForAngles(norm);
|
|
if (Array.isArray(raw)) {
|
|
return (raw as unknown[])
|
|
.filter((x): x is string => typeof x === "string")
|
|
.map((x) => x.trim())
|
|
.filter(Boolean);
|
|
}
|
|
return [];
|
|
}
|