forked from Yara724/api
Preserve damage history and current vehicle price, restore depreciation mapping, normalize inquiry/report output, and support resumable expert review with paginated case retrieval.
171 lines
5.5 KiB
TypeScript
171 lines
5.5 KiB
TypeScript
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<string, unknown>;
|
|
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,
|
|
);
|
|
}
|