stage transition bug resolved

This commit is contained in:
laxman h 2026-04-28 13:48:05 +05:30
parent f3927b4686
commit 2a289e433c
4 changed files with 159 additions and 14 deletions

View File

@ -377,7 +377,7 @@ export function ApplicationDetailsExtendedModals(props: ApplicationDetailsExtend
{!showUploadForm ? (
<div className="flex-1 flex flex-col min-h-0 space-y-4">
{getDocumentsForStage(selectedStage || '').length > 0 ? (
<div className="flex-1 overflow-auto border rounded-lg border-slate-200" data-testid="onboarding-documents-table-container">
<div className="custom-scrollbar-x-slim flex-1 overflow-auto border rounded-lg border-slate-200" data-testid="onboarding-documents-table-container">
<Table className="w-full table-auto">
<TableHeader className="bg-slate-50/80 sticky top-0 z-10">
<TableRow className="hover:bg-transparent border-b">

View File

@ -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<string, number> = {
'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<number, string[]> = {
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;

View File

@ -11,7 +11,7 @@ interface UseApplicationDetailsStageDataParams {
export function useApplicationDetailsStageData({
application,
documents,
interviews,
interviews: _interviews,
eorData,
getDeposit,
}: UseApplicationDetailsStageDataParams) {

View File

@ -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;