import { resolveStoredFileUrl } from "src/helpers/urlCreator"; import { coerceDamagedPartsMediaToArray, partLookupKey, type DamageSelectedPartV2, } from "src/helpers/outer-damage-parts"; export interface DamagedPartAuditActor { actorId: string; actorName?: string; actorType: string; } export interface StoredDamagedPartAuditRow extends DamageSelectedPartV2 { capture?: { path?: string; fileName?: string; url?: string; capturedAt?: Date | string; }; } export interface StoredDamagedPartSelectionRevision { revisionId: string; changedAt: Date; changedBy: DamagedPartAuditActor; expertProfileSnapshot?: unknown; previousSelectedParts: DamageSelectedPartV2[]; selectedParts: DamageSelectedPartV2[]; removedParts: StoredDamagedPartAuditRow[]; addedParts: StoredDamagedPartAuditRow[]; } export interface DamagedPartSelectionHistoryRow { revisionId: string; changedAt: Date | string; changedBy: DamagedPartAuditActor; expertProfileSnapshot?: unknown; removedParts: Array< DamageSelectedPartV2 & { partId: number | null; captured: boolean; url?: string; fileName?: string; capturedAt?: Date | string; currentlySelected: boolean; } >; addedParts: Array< DamageSelectedPartV2 & { partId: number | null; currentlySelected: boolean; } >; } function plainCapture(row: unknown): StoredDamagedPartAuditRow["capture"] { if (!row || typeof row !== "object") return undefined; const source = row as Record; const capture = { ...(source.path ? { path: String(source.path) } : {}), ...(source.fileName ? { fileName: String(source.fileName) } : {}), ...(source.url ? { url: String(source.url) } : {}), ...(source.capturedAt ? { capturedAt: source.capturedAt as Date | string } : {}), }; return Object.keys(capture).length > 0 ? capture : undefined; } /** * Builds one immutable audit revision before the live selected-parts/media arrays * are replaced. Returns null for a semantic no-op (including order-only changes). */ export function buildDamagedPartSelectionRevision(params: { revisionId: string; changedAt: Date; changedBy: DamagedPartAuditActor; expertProfileSnapshot?: unknown; previousParts: DamageSelectedPartV2[]; selectedParts: DamageSelectedPartV2[]; previousMedia: unknown; }): StoredDamagedPartSelectionRevision | null { const previousKeys = new Set(params.previousParts.map(partLookupKey)); const selectedKeys = new Set(params.selectedParts.map(partLookupKey)); const mediaRows = coerceDamagedPartsMediaToArray( params.previousMedia, params.previousParts, ); const removedParts = params.previousParts.flatMap((part, index) => { if (selectedKeys.has(partLookupKey(part))) return []; const capture = plainCapture(mediaRows[index]); return [{ ...part, ...(capture ? { capture } : {}) }]; }); const addedParts = params.selectedParts .filter((part) => !previousKeys.has(partLookupKey(part))) .map((part) => ({ ...part })); if (removedParts.length === 0 && addedParts.length === 0) return null; return { revisionId: params.revisionId, changedAt: params.changedAt, changedBy: params.changedBy, ...(params.expertProfileSnapshot ? { expertProfileSnapshot: params.expertProfileSnapshot } : {}), previousSelectedParts: params.previousParts.map((part) => ({ ...part })), selectedParts: params.selectedParts.map((part) => ({ ...part })), removedParts, addedParts, }; } /** Converts stored revisions into a stable, URL-enriched panel contract. */ export function serializeDamagedPartSelectionHistory(params: { history: unknown; currentSelectedParts: DamageSelectedPartV2[]; }): DamagedPartSelectionHistoryRow[] { if (!Array.isArray(params.history)) return []; const currentKeys = new Set(params.currentSelectedParts.map(partLookupKey)); return params.history .filter((revision) => revision && typeof revision === "object") .map((revision: any) => ({ revisionId: String(revision.revisionId ?? ""), changedAt: revision.changedAt, changedBy: { actorId: String(revision.changedBy?.actorId ?? ""), actorName: revision.changedBy?.actorName, actorType: String(revision.changedBy?.actorType ?? "damage_expert"), }, ...(revision.expertProfileSnapshot ? { expertProfileSnapshot: revision.expertProfileSnapshot } : {}), removedParts: (Array.isArray(revision.removedParts) ? revision.removedParts : [] ).map((part: StoredDamagedPartAuditRow) => ({ id: part.id ?? null, partId: part.id ?? null, name: part.name, side: part.side, label_fa: part.label_fa, ...(part.catalogKey ? { catalogKey: part.catalogKey } : {}), captured: !!part.capture, ...(part.capture?.fileName ? { fileName: part.capture.fileName } : {}), ...(part.capture?.capturedAt ? { capturedAt: part.capture.capturedAt } : {}), ...(resolveStoredFileUrl(part.capture) ? { url: resolveStoredFileUrl(part.capture) } : {}), currentlySelected: currentKeys.has(partLookupKey(part)), })), addedParts: (Array.isArray(revision.addedParts) ? revision.addedParts : [] ).map((part: DamageSelectedPartV2) => ({ ...part, partId: part.id ?? null, currentlySelected: currentKeys.has(partLookupKey(part)), })), })) .filter( (revision) => revision.removedParts.length > 0 || revision.addedParts.length > 0, ); }