/** * Car-angle captures must live under {@code media.carAngles} with lowercase keys * {@code front | back | left | right}. Clients sometimes send Title Case keys or * mistakenly use {@code captureType: "part"} for angles, which previously wrote * 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]; export function canonicalizeClaimCarAngleKey( raw: string | undefined | null, ): ClaimCarAngleKey | null { const t = String(raw ?? "").trim().toLowerCase(); return (CLAIM_CAR_ANGLE_KEYS as readonly string[]).includes(t) ? (t as ClaimCarAngleKey) : null; } function getCaptureBlob(data: unknown, key: string): unknown { if (data == null) return undefined; if (data instanceof Map) return (data as Map).get(key); return (data as Record)[key]; } /** Prefer carAngles; fall back to legacy mis-placed blobs under damagedParts. */ export function getClaimCarAngleCaptureBlob( carAnglesData: unknown, damagedPartsData: unknown, angle: ClaimCarAngleKey, ): unknown { return ( getCaptureBlob(carAnglesData, angle) ?? getCaptureBlob(damagedPartsData, angle) ?? getCaptureBlob( damagedPartsData, angle.charAt(0).toUpperCase() + angle.slice(1), ) ); } export function hasClaimCarAngleCapture( carAnglesData: unknown, damagedPartsData: unknown, angle: ClaimCarAngleKey, ): boolean { return getClaimCarAngleCaptureBlob(carAnglesData, damagedPartsData, angle) != null; } /** * Field names under media.damagedParts that should be removed when we persist the * same shot under media.carAngles (migration). Skips keys that are also real selected part ids. */ export function legacyAngleDamagedPartFieldKeysToUnset( canonical: ClaimCarAngleKey, rawCaptureKey: string, selectedParts: unknown, carType?: import("src/static/outer-car-parts-catalog").ClaimVehicleTypeV2, selectedOuterPartsLegacy?: unknown, ): string[] { const selected = new Set( damageSelectedPartsToLookupStrings( selectedParts, carType, selectedOuterPartsLegacy, ), ); const variants = new Set( [ rawCaptureKey, canonical, canonical.charAt(0).toUpperCase() + canonical.slice(1), ].filter(Boolean), ); const out: string[] = []; for (const k of variants) { if (canonicalizeClaimCarAngleKey(k) === null) continue; if (selected.has(k)) continue; out.push(k); } return [...new Set(out)]; } /** Same English title derived from snake_case keys as capture-requirements UI (legacy client storage). */ export function legacyDamagedPartTitleEnFromPartKey(partKey: string): string { return String(partKey) .replace(/([a-z])([A-Z])/g, "$1 $2") .replace(/_/g, " ") .trim() .replace(/\b\w/g, (c) => c.toUpperCase()); } function normalizeDamagedPartKeyLoose(s: string): string { return String(s).toLowerCase().replace(/[\s_-]/g, ""); } 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[], ); 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); if (titleEn !== pk) { b = getCaptureBlob(damagedPartsData, titleEn); if (b) return b; } const target = normalizeDamagedPartKeyLoose(pk); const keys = damagedPartsData instanceof Map ? Array.from((damagedPartsData as Map).keys()) : Object.keys(damagedPartsData as Record); for (const k of keys) { if (normalizeDamagedPartKeyLoose(k) === target) { b = getCaptureBlob(damagedPartsData, k); if (b) return b; } } return undefined; } /** * True if a capture exists for catalog/selected part key {@code partKey}, including blobs stored * under humanized keys (e.g. {@code Left Backfender} vs {@code left_backfender}). */ export function hasDamagedPartCapture( damagedPartsData: unknown, partKey: string, selectedNormalized?: DamageSelectedPartV2[], ): boolean { return ( getDamagedPartCaptureBlob(damagedPartsData, partKey, selectedNormalized) != null ); } /** * Map a client {@code captureKey} to the canonical {@code damage.selectedParts} id when unambiguous. */ export function resolveCanonicalDamagedPartStorageKey( captureKeyRaw: 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 = damageSelectedPartsToLookupStrings( selectedParts, carType, selectedOuterPartsLegacy, ); if (parts.includes(raw)) return raw; const n = normalizeDamagedPartKeyLoose(raw); const matches = parts.filter((p) => normalizeDamagedPartKeyLoose(p) === n); if (matches.length === 1) return matches[0]; return raw; }