forked from Yara724/api
Fixed claim/blame damagedParts
This commit is contained in:
@@ -5,6 +5,13 @@
|
||||
* into {@code media.damagedParts} and broke step-completion checks.
|
||||
*/
|
||||
|
||||
import type { DamageSelectedPartV2 } from "src/helpers/outer-damage-parts";
|
||||
import {
|
||||
damageSelectedPartsToLookupStrings,
|
||||
mediaRowHasCapture,
|
||||
resolvePartCaptureIndex,
|
||||
} from "src/helpers/outer-damage-parts";
|
||||
|
||||
export const CLAIM_CAR_ANGLE_KEYS = ["front", "back", "left", "right"] as const;
|
||||
export type ClaimCarAngleKey = (typeof CLAIM_CAR_ANGLE_KEYS)[number];
|
||||
|
||||
@@ -54,9 +61,17 @@ export function hasClaimCarAngleCapture(
|
||||
export function legacyAngleDamagedPartFieldKeysToUnset(
|
||||
canonical: ClaimCarAngleKey,
|
||||
rawCaptureKey: string,
|
||||
selectedParts: string[],
|
||||
selectedParts: unknown,
|
||||
carType?: import("src/static/outer-car-parts-catalog").ClaimVehicleTypeV2,
|
||||
selectedOuterPartsLegacy?: unknown,
|
||||
): string[] {
|
||||
const selected = new Set((selectedParts ?? []).map((p) => String(p)));
|
||||
const selected = new Set(
|
||||
damageSelectedPartsToLookupStrings(
|
||||
selectedParts,
|
||||
carType,
|
||||
selectedOuterPartsLegacy,
|
||||
),
|
||||
);
|
||||
const variants = new Set(
|
||||
[
|
||||
rawCaptureKey,
|
||||
@@ -89,9 +104,23 @@ function normalizeDamagedPartKeyLoose(s: string): string {
|
||||
export function getDamagedPartCaptureBlob(
|
||||
damagedPartsData: unknown,
|
||||
partKey: string,
|
||||
selectedNormalized?: DamageSelectedPartV2[],
|
||||
): unknown {
|
||||
const pk = String(partKey ?? "").trim();
|
||||
if (!pk || damagedPartsData == null) return undefined;
|
||||
if (Array.isArray(damagedPartsData)) {
|
||||
const norm = selectedNormalized ?? [];
|
||||
const idx = resolvePartCaptureIndex(
|
||||
pk,
|
||||
norm,
|
||||
damagedPartsData as Record<string, unknown>[],
|
||||
);
|
||||
if (idx >= 0) {
|
||||
const row = (damagedPartsData as unknown[])[idx];
|
||||
if (mediaRowHasCapture(row)) return row;
|
||||
}
|
||||
return undefined;
|
||||
}
|
||||
let b = getCaptureBlob(damagedPartsData, pk);
|
||||
if (b) return b;
|
||||
const titleEn = legacyDamagedPartTitleEnFromPartKey(pk);
|
||||
@@ -120,8 +149,12 @@ export function getDamagedPartCaptureBlob(
|
||||
export function hasDamagedPartCapture(
|
||||
damagedPartsData: unknown,
|
||||
partKey: string,
|
||||
selectedNormalized?: DamageSelectedPartV2[],
|
||||
): boolean {
|
||||
return getDamagedPartCaptureBlob(damagedPartsData, partKey) != null;
|
||||
return (
|
||||
getDamagedPartCaptureBlob(damagedPartsData, partKey, selectedNormalized) !=
|
||||
null
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -129,11 +162,17 @@ export function hasDamagedPartCapture(
|
||||
*/
|
||||
export function resolveCanonicalDamagedPartStorageKey(
|
||||
captureKeyRaw: string,
|
||||
selectedParts: string[],
|
||||
selectedParts: unknown,
|
||||
carType?: import("src/static/outer-car-parts-catalog").ClaimVehicleTypeV2,
|
||||
selectedOuterPartsLegacy?: unknown,
|
||||
): string {
|
||||
const raw = String(captureKeyRaw ?? "").trim();
|
||||
if (!raw) return raw;
|
||||
const parts = (selectedParts ?? []).map((p) => String(p).trim()).filter(Boolean);
|
||||
const parts = damageSelectedPartsToLookupStrings(
|
||||
selectedParts,
|
||||
carType,
|
||||
selectedOuterPartsLegacy,
|
||||
);
|
||||
if (parts.includes(raw)) return raw;
|
||||
const n = normalizeDamagedPartKeyLoose(raw);
|
||||
const matches = parts.filter((p) => normalizeDamagedPartKeyLoose(p) === n);
|
||||
|
||||
@@ -1,5 +1,8 @@
|
||||
import { ClaimCaseStatus } from "src/Types&Enums/claim-request-management/claim-case-status.enum";
|
||||
import { ClaimWorkflowStep } from "src/Types&Enums/claim-request-management/claim-workflow-steps.enum";
|
||||
import { getDamagedPartCaptureBlob } from "./claim-car-angle-media";
|
||||
import { normalizeDamageSelectedParts } from "./outer-damage-parts";
|
||||
import { ClaimVehicleTypeV2 } from "src/static/outer-car-parts-catalog";
|
||||
import { canonicalizeResendDocumentKey } from "./claim-resend-document-keys";
|
||||
|
||||
export function resendRequestHasPayload(r: {
|
||||
@@ -59,8 +62,12 @@ export function isRequiredDocumentUploaded(claim: any, documentKey: string): boo
|
||||
export function hasDamagedPartCapture(claim: any, partKey: string): boolean {
|
||||
const data = claim?.media?.damagedParts;
|
||||
if (!data) return false;
|
||||
const cap = data instanceof Map ? data.get(partKey) : data[partKey];
|
||||
return !!cap;
|
||||
const norm = normalizeDamageSelectedParts(
|
||||
claim?.damage?.selectedParts,
|
||||
claim?.vehicle?.carType as ClaimVehicleTypeV2 | undefined,
|
||||
claim?.damage?.selectedOuterParts,
|
||||
);
|
||||
return getDamagedPartCaptureBlob(data, partKey, norm) != null;
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
700
src/helpers/outer-damage-parts.ts
Normal file
700
src/helpers/outer-damage-parts.ts
Normal file
@@ -0,0 +1,700 @@
|
||||
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 [];
|
||||
}
|
||||
Reference in New Issue
Block a user