This commit is contained in:
SepehrYahyaee
2026-05-18 11:00:53 +03:30
parent 7ff3e9fd10
commit e1954cdb37
8 changed files with 327 additions and 34 deletions

View File

@@ -0,0 +1,113 @@
import { Types } from "mongoose";
import { ExpertFileActivityType } from "src/users/entities/schema/expert-file-activity.schema";
import {
blameCaseAccessibleToExpert,
claimCaseTouchesClient,
} from "src/helpers/tenant-scope";
export interface ExpertFileActivityEventRow {
fileId: Types.ObjectId | string;
eventType: ExpertFileActivityType;
occurredAt: Date;
}
/**
* Distinct file ids where the expert is actively checking (CHECKED, not UNCHECKED)
* or has handled the file (HANDLED), replayed in chronological order.
*/
export function expertPortfolioFileIdsFromActivityEvents(
events: ExpertFileActivityEventRow[],
): Set<string> {
const sorted = [...events].sort(
(a, b) =>
a.occurredAt.getTime() - b.occurredAt.getTime() ||
String(a.fileId).localeCompare(String(b.fileId)),
);
const stateByFile = new Map<string, { checked: boolean; handled: boolean }>();
for (const e of sorted) {
const fid = String(e.fileId);
const prev = stateByFile.get(fid) ?? { checked: false, handled: false };
if (e.eventType === ExpertFileActivityType.CHECKED) {
if (!prev.handled) prev.checked = true;
} else if (e.eventType === ExpertFileActivityType.UNCHECKED) {
prev.checked = false;
} else if (e.eventType === ExpertFileActivityType.HANDLED) {
prev.handled = true;
prev.checked = false;
}
stateByFile.set(fid, prev);
}
const ids = new Set<string>();
for (const [fid, st] of stateByFile) {
if (st.handled || st.checked) ids.add(fid);
}
return ids;
}
export function objectIdsFromStringSet(ids: Set<string>): Types.ObjectId[] {
return [...ids]
.filter((id) => Types.ObjectId.isValid(id))
.map((id) => new Types.ObjectId(id));
}
/** Damage-expert claim portfolio: assigned (lock/check) or substantive work on the file. */
export function claimDocInExpertPortfolio(
doc: Record<string, unknown>,
expertId: string,
activityFileIds: Set<string>,
clientKey: string,
): boolean {
if (!claimCaseTouchesClient(doc, clientKey)) return false;
const id = String(doc._id ?? "");
if (activityFileIds.has(id)) return true;
const lockedBy = (doc.workflow as { lockedBy?: { actorId?: unknown } } | undefined)
?.lockedBy?.actorId;
if (lockedBy != null && String(lockedBy) === String(expertId)) return true;
const evaluation = doc.evaluation as Record<string, unknown> | undefined;
for (const key of ["damageExpertReply", "damageExpertReplyFinal"] as const) {
const reply = evaluation?.[key] as
| { actorDetail?: { actorId?: unknown } }
| undefined;
const actorId = reply?.actorDetail?.actorId;
if (actorId != null && String(actorId) === String(expertId)) return true;
}
const resendBy = (
evaluation?.damageExpertResend as { requestedByExpertId?: unknown } | undefined
)?.requestedByExpertId;
if (resendBy != null && String(resendBy) === String(expertId)) return true;
return false;
}
/** Field-expert blame portfolio: initiated, decided, locked, or file-activity touch. */
export function blameDocInExpertPortfolio(
doc: Record<string, unknown>,
actor: { sub: string; clientKey?: string },
activityFileIds: Set<string>,
): boolean {
if (!blameCaseAccessibleToExpert(doc, actor)) return false;
const expertId = String(actor.sub);
const id = String(doc._id ?? "");
if (activityFileIds.has(id)) return true;
if (doc.expertInitiated && doc.initiatedByFieldExpertId) {
return String(doc.initiatedByFieldExpertId) === expertId;
}
const expert = doc.expert as Record<string, unknown> | undefined;
const decidedBy = (expert?.decision as { decidedByExpertId?: unknown } | undefined)
?.decidedByExpertId;
if (decidedBy != null && String(decidedBy) === expertId) return true;
const lockedBy =
(expert?.resend as { requestedByExpertId?: unknown } | undefined)
?.requestedByExpertId ??
(doc.workflow as { lockedBy?: { actorId?: unknown } } | undefined)?.lockedBy
?.actorId;
if (lockedBy != null && String(lockedBy) === expertId) return true;
return false;
}