From 2a289e433c3ab0d5988b2dc1f24494316fb11e0f Mon Sep 17 00:00:00 2001 From: laxman h Date: Tue, 28 Apr 2026 13:48:05 +0530 Subject: [PATCH] stage transition bug resolved --- .../ApplicationDetailsExtendedModals.tsx | 2 +- .../hooks/useApplicationDetailsPermissions.ts | 146 ++++++++++++++++-- .../hooks/useApplicationDetailsStageData.ts | 2 +- src/styles/globals.css | 23 +++ 4 files changed, 159 insertions(+), 14 deletions(-) diff --git a/src/features/onboarding/components/application-details/ApplicationDetailsExtendedModals.tsx b/src/features/onboarding/components/application-details/ApplicationDetailsExtendedModals.tsx index 6b60cef..1eb584d 100644 --- a/src/features/onboarding/components/application-details/ApplicationDetailsExtendedModals.tsx +++ b/src/features/onboarding/components/application-details/ApplicationDetailsExtendedModals.tsx @@ -377,7 +377,7 @@ export function ApplicationDetailsExtendedModals(props: ApplicationDetailsExtend {!showUploadForm ? (
{getDocumentsForStage(selectedStage || '').length > 0 ? ( -
+
diff --git a/src/features/onboarding/hooks/useApplicationDetailsPermissions.ts b/src/features/onboarding/hooks/useApplicationDetailsPermissions.ts index 48eef10..bc6ebce 100644 --- a/src/features/onboarding/hooks/useApplicationDetailsPermissions.ts +++ b/src/features/onboarding/hooks/useApplicationDetailsPermissions.ts @@ -6,6 +6,106 @@ interface UseApplicationDetailsPermissionsParams { eorProgress: number; } +function sameUserId(a: unknown, b: unknown): boolean { + if (a == null || b == null) return false; + return String(a).trim() === String(b).trim(); +} + +/** Backend / DB may use different casing (e.g. default `scheduled` vs created `Scheduled`). */ +function normalizeInterviewStatus(status: unknown): string { + return String(status ?? '') + .trim() + .toLowerCase() + .replace(/\s+/g, ' '); +} + +function isActiveInterviewStatus(status: unknown): boolean { + const n = normalizeInterviewStatus(status); + if (!n) return false; + return ( + n === 'scheduled' || + n === 'rescheduled' || + n === 'pending' || + n === 'in progress' || + n === 'inprogress' + ); +} + +function isCompletedInterviewStatus(status: unknown): boolean { + return normalizeInterviewStatus(status) === 'completed'; +} + +function userIsInterviewParticipant(interview: any, userId: unknown): boolean { + if (!userId || !interview?.participants?.length) return false; + return interview.participants.some( + (p: any) => sameUserId(p.userId, userId) || sameUserId(p.user?.id, userId), + ); +} + +/** Which interview level the application is currently in (for feedback UI). */ +function inferInterviewLevelFromApplicationStatus(status: unknown): number | undefined { + const s = String(status ?? '').trim(); + const map: Record = { + 'Level 1 Interview Pending': 1, + 'Level 1 Recommended': 1, + 'Level 2 Interview Pending': 2, + 'Level 2 Recommended': 2, + 'Level 3 Interview Pending': 3, + 'Level 3 Recommended': 3, + }; + return map[s]; +} + +function normalizeRoleToken(value: unknown): string { + return String(value ?? '') + .trim() + .toLowerCase() + .replace(/[_\s-]+/g, ''); +} + +/** + * Allow feedback when scheduling forgot to attach this user as InterviewParticipant but they are a + * designated evaluator for this level (matches backend interview policy / schedule prefill roles). + */ +function userRoleEligibleForInterviewLevel(user: any, level: number): boolean { + if (!user) return false; + const privilegedRoles = ['Super Admin', 'DD Admin']; + if ( + privilegedRoles.includes(String(user.role ?? '')) || + privilegedRoles.includes(String(user.roleCode ?? '')) + ) { + return true; + } + + const rolesByLevel: Record = { + 1: ['DD-ZM', 'DD ZM', 'RBM'], + 2: ['DD Lead', 'ZBH'], + 3: ['NBH', 'DD Head'], + }; + const allowedRaw = rolesByLevel[level]; + if (!allowedRaw?.length) return false; + + const allowed = allowedRaw.map(normalizeRoleToken); + const candidates = [ + user.role, + user.roleCode, + user.roleName, + user.role?.roleCode, + user.role?.roleName, + ] + .filter(Boolean) + .map(normalizeRoleToken); + + for (const c of candidates) { + if (!c) continue; + for (const a of allowed) { + if (!a) continue; + if (c === a || c.includes(a) || a.includes(c)) return true; + } + } + return false; +} + export function useApplicationDetailsPermissions({ application, interviews, @@ -15,24 +115,46 @@ export function useApplicationDetailsPermissions({ }: UseApplicationDetailsPermissionsParams) { const interviewsList = Array.isArray(interviews) ? interviews : []; - const activeInterviewForUser = interviewsList.find((i: any) => - ['Scheduled', 'Rescheduled', 'Pending', 'In Progress'].includes(i.status) && - i.participants?.some((p: any) => p.userId === currentUser?.id) - ); + const stageInterviewLevel = inferInterviewLevelFromApplicationStatus(application?.status); - const lastInterviewForUser = [...interviewsList].reverse().find((i: any) => - i.participants?.some((p: any) => p.userId === currentUser?.id) - ); + /** Prefer participant match on the interview row that matches current application stage when possible. */ + const participantActiveInterview = + (stageInterviewLevel != null + ? interviewsList.find( + (i: any) => + isActiveInterviewStatus(i.status) && + userIsInterviewParticipant(i, currentUser?.id) && + Number(i.level) === stageInterviewLevel, + ) + : undefined) ?? + interviewsList.find( + (i: any) => isActiveInterviewStatus(i.status) && userIsInterviewParticipant(i, currentUser?.id), + ); - const currentUserEvaluation = (activeInterviewForUser || lastInterviewForUser)?.evaluations?.find( - (e: any) => e.evaluatorId === currentUser?.id - ); + /** Same stage + active interview + evaluator role — covers missing / partial participant rows. */ + const roleFallbackActiveInterview = + stageInterviewLevel != null && + currentUser && + userRoleEligibleForInterviewLevel(currentUser, stageInterviewLevel) + ? interviewsList.find( + (i: any) => + Number(i.level) === stageInterviewLevel && isActiveInterviewStatus(i.status), + ) + : undefined; + + const activeInterviewForUser = participantActiveInterview ?? roleFallbackActiveInterview; + + const lastInterviewForUser = interviewsList.find((i: any) => userIsInterviewParticipant(i, currentUser?.id)); + + const currentUserEvaluation = + activeInterviewForUser?.evaluations?.find((e: any) => sameUserId(e.evaluatorId, currentUser?.id)) ?? + lastInterviewForUser?.evaluations?.find((e: any) => sameUserId(e.evaluatorId, currentUser?.id)); const isInterviewCompleted = (level: number) => - interviewsList.some((i: any) => Number(i.level) === level && i.status === 'Completed'); + interviewsList.some((i: any) => Number(i.level) === level && isCompletedInterviewStatus(i.status)); const isInterviewActive = (level: number) => - interviewsList.some((i: any) => Number(i.level) === level && i.status === 'Scheduled'); + interviewsList.some((i: any) => Number(i.level) === level && isActiveInterviewStatus(i.status)); const hasSubmittedFeedback = !!currentUserEvaluation; diff --git a/src/features/onboarding/hooks/useApplicationDetailsStageData.ts b/src/features/onboarding/hooks/useApplicationDetailsStageData.ts index 032e06a..ddec086 100644 --- a/src/features/onboarding/hooks/useApplicationDetailsStageData.ts +++ b/src/features/onboarding/hooks/useApplicationDetailsStageData.ts @@ -11,7 +11,7 @@ interface UseApplicationDetailsStageDataParams { export function useApplicationDetailsStageData({ application, documents, - interviews, + interviews: _interviews, eorData, getDeposit, }: UseApplicationDetailsStageDataParams) { diff --git a/src/styles/globals.css b/src/styles/globals.css index 5133591..982aed0 100644 --- a/src/styles/globals.css +++ b/src/styles/globals.css @@ -298,6 +298,29 @@ html { scrollbar-color: #e2e8f0 transparent; } +/* Extra-thin, subtle horizontal scrollbar (documents modal tables) */ +.custom-scrollbar-x-slim::-webkit-scrollbar { + height: 3px; +} + +.custom-scrollbar-x-slim::-webkit-scrollbar-track { + background: transparent; +} + +.custom-scrollbar-x-slim::-webkit-scrollbar-thumb { + background: #f1f5f9; + border-radius: 9999px; +} + +.custom-scrollbar-x-slim::-webkit-scrollbar-thumb:hover { + background: #e2e8f0; +} + +.custom-scrollbar-x-slim { + scrollbar-width: thin; + scrollbar-color: #f1f5f9 transparent; +} + /* Extra-thin, light vertical scrollbar (e.g. modals) */ .custom-scrollbar-slim::-webkit-scrollbar { width: 2px;