stage transition bug resolved
This commit is contained in:
parent
f3927b4686
commit
2a289e433c
@ -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">
|
||||
|
||||
@ -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;
|
||||
|
||||
|
||||
@ -11,7 +11,7 @@ interface UseApplicationDetailsStageDataParams {
|
||||
export function useApplicationDetailsStageData({
|
||||
application,
|
||||
documents,
|
||||
interviews,
|
||||
interviews: _interviews,
|
||||
eorData,
|
||||
getDeposit,
|
||||
}: UseApplicationDetailsStageDataParams) {
|
||||
|
||||
@ -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;
|
||||
|
||||
Loading…
Reference in New Issue
Block a user