diff --git a/src/expert-insurer/expert-insurer.controller.ts b/src/expert-insurer/expert-insurer.controller.ts index 9d11423..71d9dc6 100644 --- a/src/expert-insurer/expert-insurer.controller.ts +++ b/src/expert-insurer/expert-insurer.controller.ts @@ -240,6 +240,23 @@ export class ExpertInsurerController { ); } + @Get("files/:publicId/timeline") + @ApiParam({ name: "publicId" }) + @ApiOperation({ + summary: "Activity timeline for a case", + description: + "Returns a chronological list of all history events for the blame and/or claim associated with the given publicId. Each event has: source, type, timestamp, actor, metadata.", + }) + async getFileTimeline( + @CurrentUser() insurer, + @Param("publicId") publicId: string, + ) { + return await this.expertInsurerService.getFileTimeline( + insurer.clientKey, + publicId, + ); + } + @Get("files/:publicId") @ApiParam({ name: "publicId" }) async getFileDetailsByPublicId( diff --git a/src/expert-insurer/expert-insurer.service.ts b/src/expert-insurer/expert-insurer.service.ts index 0bf526d..8b82424 100644 --- a/src/expert-insurer/expert-insurer.service.ts +++ b/src/expert-insurer/expert-insurer.service.ts @@ -75,6 +75,7 @@ import { extractExpertNamesFromBlame, extractExpertNamesFromClaim, } from "./helper/insurer.helper"; +import { getEventFaLabel } from "./helper/timeline-fa-labels"; import { buildEnrichedDamagedParts } from "src/expert-claim/dto/claim-damaged-part.enricher"; @Injectable() @@ -2088,4 +2089,77 @@ export class ExpertInsurerService { waiting_for_documents_resend: counts.WAITING_FOR_DOCUMENT_RESEND ?? 0, }; } + + async getFileTimeline( + insurerId: string, + publicId: string, + ): Promise<{ publicId: string; events: object[] }> { + if (!publicId?.trim()) { + throw new BadRequestException("publicId is required"); + } + + const id = this.getClientId(insurerId); + const [blameFiles, claimFiles] = await Promise.all([ + this.getClientBlameFiles(id), + this.getClientClaimFiles(id), + ]); + + const blame = blameFiles.find( + (b) => String((b as any).publicId) === publicId, + ); + const claim = claimFiles.find( + (c) => String((c as any).publicId) === publicId, + ); + + if (!blame && !claim) { + throw new NotFoundException("File not found for this publicId"); + } + + const events: object[] = []; + + if (blame) { + const blameDoc = await this.blameRequestDbService.findById( + String((blame as any)._id), + ); + const blameHistory: any[] = (blameDoc as any)?.history ?? []; + for (const ev of blameHistory) { + events.push({ + source: "blame", + type: ev.type, + faLabel: getEventFaLabel(ev), + timestamp: ev.timestamp, + actor: ev.actor ?? null, + metadata: ev.metadata ?? null, + }); + } + } + + if (claim) { + const claimId = String((claim as any)._id); + const rows = (await this.claimCaseDbService.find( + { _id: new Types.ObjectId(claimId) }, + { lean: true, select: "history createdAt" }, + )) as Record[]; + const claimHistory: any[] = (rows[0] as any)?.history ?? []; + for (const ev of claimHistory) { + events.push({ + source: "claim", + type: ev.type, + faLabel: getEventFaLabel(ev), + timestamp: ev.timestamp, + actor: ev.actor ?? null, + metadata: ev.metadata ?? null, + }); + } + } + + events.sort((a: any, b: any) => { + const ta = a.timestamp ? new Date(a.timestamp).getTime() : 0; + const tb = b.timestamp ? new Date(b.timestamp).getTime() : 0; + return ta - tb; + }); + + // Resolve insurer's client name for context if needed — just return raw events + return { publicId, events }; + } } diff --git a/src/expert-insurer/helper/timeline-fa-labels.ts b/src/expert-insurer/helper/timeline-fa-labels.ts new file mode 100644 index 0000000..2176f53 --- /dev/null +++ b/src/expert-insurer/helper/timeline-fa-labels.ts @@ -0,0 +1,196 @@ +/** + * Persian (Farsi) display labels for every timeline event `type` that can + * appear in a blame or claim history array. + * + * For generic `STEP_COMPLETED` / `V3_STEP_COMPLETED` events the label is + * derived from the `metadata.stepKey` value; those keys are listed at the + * bottom under STEP_KEY_FA_LABELS. + * + * Usage: + * const label = EVENT_TYPE_FA_LABELS[event.type] + * ?? resolveStepCompletedFaLabel(event) + * ?? event.type; + */ + +// --------------------------------------------------------------------------- +// Blame-phase events (recorded on BlameRequest.history) +// --------------------------------------------------------------------------- +export const EVENT_TYPE_FA_LABELS: Record = { + // File creation + FILE_CREATED_BY_REGISTRAR: "پرونده توسط ثبت‌کننده ایجاد شد", + FILE_CREATED_BY_FIELD_EXPERT: "پرونده توسط کارشناس میدانی ایجاد شد", + + // Expert link / OTP flow + LINK_SENT: "لینک ارسال شد", + PARTY_OTP_SENT: "کد تأیید ارسال شد", + PARTY_OTP_VERIFIED: "کد تأیید تأیید شد", + PARTY_OTPS_VERIFIED: "کدهای تأیید هر دو طرف تأیید شدند", + SECOND_PARTY_OTP_SENT: "کد تأیید طرف دوم ارسال شد", + SECOND_PARTY_OTP_VERIFIED_ADVANCED: "کد طرف دوم تأیید و پیشرفت انجام شد", + + // Confession / accident type + FIRST_BLAME_CONFESSION_SUBMITTED: "اقرار اولیه طرف اول ثبت شد", + CAR_BODY_ACCIDENT_TYPE_SUBMITTED: "نوع تصادف بدنه خودرو ثبت شد", + AUTO_ADVANCED_TO_CAR_BODY_FORM: "پیشرفت خودکار به فرم بدنه خودرو", + AUTO_CONFESSION_SKIPPED: "مرحله اقرار به‌صورت خودکار رد شد", + + // First video + FIRST_VIDEO_UPLOADED: "ویدیوی اول بارگذاری شد", + + // Second party invitation + SECOND_PARTY_INVITED: "طرف دوم دعوت شد", + + // Accident fields / expert in-person completion + ACCIDENT_FIELDS_SAVED_ADVANCED_TO_SIGNATURES: + "اطلاعات تصادف ذخیره و به مرحله امضاها پیشرفت شد", + EXPERT_COMPLETED_CAR_BODY_FORM_V2: "کارشناس فرم بدنه خودرو را تکمیل کرد", + EXPERT_COMPLETED_THIRD_PARTY_FORM_V2: "کارشناس فرم شخص ثالث را تکمیل کرد", + EXPERT_ADDED_LOCATIONS_V2: "کارشناس موقعیت مکانی را ثبت کرد", + EXPERT_UPLOADED_VIDEO_V2: "کارشناس ویدیو را بارگذاری کرد", + EXPERT_UPLOADED_VOICE_V2: "کارشناس صدا را بارگذاری کرد", + + // Party rejection / disagreement + PARTY_REJECTED_EXPERT_DECISION: "طرف حساب با نظر کارشناس مخالفت کرد", + PARTIES_DISAGREED_ON_EXPERT_DECISION: "طرفین با نظر کارشناس توافق نکردند", + + // Document resend (blame) + BLAME_DOCUMENT_RESEND_STARTED: "درخواست ارسال مجدد مدارک پرونده شروع شد", + BLAME_DOCUMENT_RESEND_CLEARED: "ارسال مجدد مدارک پرونده پاک‌سازی شد", + + // Expert assignment (blame) + BLAME_ASSIGNED: "پرونده به کارشناس تخصیص داده شد", + AUTO_ASSIGNED_TO_EXPERT: "پرونده به‌صورت خودکار به کارشناس تخصیص یافت", + + // Auto-assignment helpers + READY_FOR_EXPERT_EVALUATION: "پرونده آماده ارزیابی کارشناس شد", + SECOND_PARTY_DESCRIPTION_COMPLETED: "توضیحات طرف دوم تکمیل شد", + + // V3 blame steps + V3_PARTY_SIGNATURE_RECORDED: "امضای طرف ثبت شد", + V3_ACCIDENT_FIELDS_SAVED: "اطلاعات تصادف ثبت شد", + V3_BLAME_ACCIDENT_VIDEO_UPLOADED: "ویدیوی تصادف بارگذاری شد", + V3_PARTY_VOICE_UPLOADED: "صدای طرف بارگذاری شد", + V3_PARTY_LOCATION_SAVED: "موقعیت مکانی طرف ذخیره شد", + V3_PARTY_DESCRIPTION_SAVED: "توضیحات طرف ذخیره شد", + V3_CAR_BODY_ACCIDENT_TYPE_SAVED: "نوع تصادف بدنه خودرو ذخیره شد", + + // V5 blame steps + V5_BLAME_ACCIDENT_VIDEO_UPLOADED: "ویدیوی تصادف (نسخه ۵) بارگذاری شد", + V5_FILE_MAKER_APPROVED: "پرونده‌ساز پرونده را تأیید کرد", + V5_FILE_MAKER_REJECTED: "پرونده‌ساز پرونده را رد کرد", + + // --------------------------------------------------------------------------- + // Claim-phase events (recorded on ClaimCase.history) + // --------------------------------------------------------------------------- + + // Claim creation + CLAIM_CREATED: "پرونده خسارت ایجاد شد", + + // Expert assignment (claim) + CLAIM_ASSIGNED: "پرونده خسارت به کارشناس ارزیابی تخصیص داده شد", + + // Expert actions on claim + EXPERT_RESEND_REQUESTED: "کارشناس درخواست ارسال مجدد مدارک داد", + EXPERT_RESEND_FULFILLED: "مدارک درخواستی ارسال شد", + IN_PERSON_VISIT_REQUESTED: "کارشناس بازدید حضوری درخواست کرد", + EXPERT_DAMAGED_PARTS_UPDATED: "کارشناس قطعات آسیب‌دیده را به‌روزرسانی کرد", + + // Workflow steps (generic) + STEP_COMPLETED: "مرحله تکمیل شد", + V3_STEP_COMPLETED: "مرحله تکمیل شد", + + // Documents & media + DOCUMENT_UPLOADED: "مدرک بارگذاری شد", + VIDEO_CAPTURE_UPLOADED: "تصویر ویدیویی بارگذاری شد", + ALL_FACTORS_UPLOADED_PENDING_VALIDATION: + "تمام فاکتورها بارگذاری شدند و در انتظار تأیید هستند", + + // User responses + USER_OBJECTION_SUBMITTED: "اعتراض کاربر ثبت شد", + USER_RATING_SUBMITTED: "امتیاز کاربر ثبت شد", + + // Owner insurer approval + OWNER_SIGNED_INSURER_APPROVAL: "صاحب خودرو قرارداد بیمه را امضا کرد", + OWNER_REJECTED_INSURER_APPROVAL_PRICING: "صاحب خودرو قیمت‌گذاری بیمه را رد کرد", + OWNER_SIGNED_PRICED_PARTS_PENDING_FACTOR_UPLOAD: + "صاحب خودرو قطعات قیمت‌گذاری‌شده را امضا کرد و در انتظار بارگذاری فاکتور", + OWNER_REJECTED_PRICED_PARTS_BEFORE_FACTORS: + "صاحب خودرو قطعات قیمت‌گذاری‌شده را پیش از فاکتور رد کرد", + + // Fanavaran sync + FANAVARAN_AUTO_SUBMIT_SUCCEEDED: "ارسال خودکار به فناوران موفق بود", + FANAVARAN_AUTO_SUBMIT_FAILED: "ارسال خودکار به فناوران ناموفق بود", + FANAVARAN_EARLY_AUTO_SUBMIT_SUCCEEDED: "ارسال زودهنگام خودکار به فناوران موفق بود", + FANAVARAN_EARLY_AUTO_SUBMIT_FAILED: "ارسال زودهنگام خودکار به فناوران ناموفق بود", + FANAVARAN_EXPERTISE_AUTO_SUBMIT_SUCCEEDED: + "ارسال خودکار کارشناسی به فناوران موفق بود", + FANAVARAN_EXPERTISE_AUTO_SUBMIT_FAILED: + "ارسال خودکار کارشناسی به فناوران ناموفق بود", + FANAVARAN_ATTACHMENT_AUTO_SUBMIT_SUCCEEDED: + "ارسال خودکار پیوست به فناوران موفق بود", + FANAVARAN_ATTACHMENT_AUTO_SUBMIT_FAILED: + "ارسال خودکار پیوست به فناوران ناموفق بود", + FANAVARAN_DAMAGE_CASE_AUTO_SUBMIT_SUCCEEDED: + "ارسال خودکار پرونده خسارت به فناوران موفق بود", + FANAVARAN_DAMAGE_CASE_AUTO_SUBMIT_FAILED: + "ارسال خودکار پرونده خسارت به فناوران ناموفق بود", + + // V5 claim + V5_HELD_FOR_FILE_MAKER_APPROVAL: "پرونده در انتظار تأیید پرونده‌ساز متوقف شد", +}; + +// --------------------------------------------------------------------------- +// Persian labels for `metadata.stepKey` inside STEP_COMPLETED events +// --------------------------------------------------------------------------- +export const STEP_KEY_FA_LABELS: Record = { + // Claim workflow steps + CLAIM_CREATED: "ایجاد پرونده خسارت", + SELECT_OUTER_PARTS: "انتخاب قطعات بیرونی آسیب‌دیده", + SELECT_OTHER_PARTS: "انتخاب سایر اطلاعات و قطعات", + CAPTURE_PART_DAMAGES: "عکس‌برداری از آسیب‌های قطعات", + UPLOAD_REQUIRED_DOCUMENTS: "بارگذاری مدارک مورد نیاز", + USER_SUBMISSION_COMPLETE: "ارسال اطلاعات توسط کاربر", + USER_EXPERT_RESEND: "ارسال مجدد مدارک توسط کاربر", + EXPERT_DAMAGE_ASSESSMENT: "ارزیابی خسارت توسط کارشناس", + EXPERT_FINAL_REPLY: "پاسخ نهایی کارشناس", + EXPERT_COST_EVALUATION: "ارزیابی هزینه توسط کارشناس", + OWNER_UPLOAD_FACTOR_DOCUMENTS: "بارگذاری فاکتورهای تعمیر توسط مالک", + INSURER_REVIEW: "بررسی توسط بیمه‌گر", + CLAIM_COMPLETED: "پرونده خسارت تکمیل شد", +}; + +/** + * For `STEP_COMPLETED` and `V3_STEP_COMPLETED` events whose final label + * depends on the `metadata.stepKey` value, this function returns the + * most specific Persian label available. + */ +export function resolveStepCompletedFaLabel(event: { + type: string; + metadata?: any; +}): string | undefined { + if ( + event.type !== "STEP_COMPLETED" && + event.type !== "V3_STEP_COMPLETED" + ) { + return undefined; + } + const stepKey: string | undefined = event.metadata?.stepKey; + if (stepKey && STEP_KEY_FA_LABELS[stepKey]) { + return STEP_KEY_FA_LABELS[stepKey]; + } + return "مرحله تکمیل شد"; +} + +/** + * Returns the best Persian label for any timeline event. + */ +export function getEventFaLabel(event: { + type: string; + metadata?: any; +}): string { + return ( + resolveStepCompletedFaLabel(event) ?? + EVENT_TYPE_FA_LABELS[event.type] ?? + event.type + ); +}