stage transition bug resolved
This commit is contained in:
parent
f3927b4686
commit
2a289e433c
@ -377,7 +377,7 @@ export function ApplicationDetailsExtendedModals(props: ApplicationDetailsExtend
|
|||||||
{!showUploadForm ? (
|
{!showUploadForm ? (
|
||||||
<div className="flex-1 flex flex-col min-h-0 space-y-4">
|
<div className="flex-1 flex flex-col min-h-0 space-y-4">
|
||||||
{getDocumentsForStage(selectedStage || '').length > 0 ? (
|
{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">
|
<Table className="w-full table-auto">
|
||||||
<TableHeader className="bg-slate-50/80 sticky top-0 z-10">
|
<TableHeader className="bg-slate-50/80 sticky top-0 z-10">
|
||||||
<TableRow className="hover:bg-transparent border-b">
|
<TableRow className="hover:bg-transparent border-b">
|
||||||
|
|||||||
@ -6,6 +6,106 @@ interface UseApplicationDetailsPermissionsParams {
|
|||||||
eorProgress: number;
|
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({
|
export function useApplicationDetailsPermissions({
|
||||||
application,
|
application,
|
||||||
interviews,
|
interviews,
|
||||||
@ -15,24 +115,46 @@ export function useApplicationDetailsPermissions({
|
|||||||
}: UseApplicationDetailsPermissionsParams) {
|
}: UseApplicationDetailsPermissionsParams) {
|
||||||
const interviewsList = Array.isArray(interviews) ? interviews : [];
|
const interviewsList = Array.isArray(interviews) ? interviews : [];
|
||||||
|
|
||||||
const activeInterviewForUser = interviewsList.find((i: any) =>
|
const stageInterviewLevel = inferInterviewLevelFromApplicationStatus(application?.status);
|
||||||
['Scheduled', 'Rescheduled', 'Pending', 'In Progress'].includes(i.status) &&
|
|
||||||
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 lastInterviewForUser = [...interviewsList].reverse().find((i: any) =>
|
/** Same stage + active interview + evaluator role — covers missing / partial participant rows. */
|
||||||
i.participants?.some((p: any) => p.userId === currentUser?.id)
|
const roleFallbackActiveInterview =
|
||||||
);
|
stageInterviewLevel != null &&
|
||||||
|
currentUser &&
|
||||||
|
userRoleEligibleForInterviewLevel(currentUser, stageInterviewLevel)
|
||||||
|
? interviewsList.find(
|
||||||
|
(i: any) =>
|
||||||
|
Number(i.level) === stageInterviewLevel && isActiveInterviewStatus(i.status),
|
||||||
|
)
|
||||||
|
: undefined;
|
||||||
|
|
||||||
const currentUserEvaluation = (activeInterviewForUser || lastInterviewForUser)?.evaluations?.find(
|
const activeInterviewForUser = participantActiveInterview ?? roleFallbackActiveInterview;
|
||||||
(e: any) => e.evaluatorId === currentUser?.id
|
|
||||||
);
|
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) =>
|
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) =>
|
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;
|
const hasSubmittedFeedback = !!currentUserEvaluation;
|
||||||
|
|
||||||
|
|||||||
@ -11,7 +11,7 @@ interface UseApplicationDetailsStageDataParams {
|
|||||||
export function useApplicationDetailsStageData({
|
export function useApplicationDetailsStageData({
|
||||||
application,
|
application,
|
||||||
documents,
|
documents,
|
||||||
interviews,
|
interviews: _interviews,
|
||||||
eorData,
|
eorData,
|
||||||
getDeposit,
|
getDeposit,
|
||||||
}: UseApplicationDetailsStageDataParams) {
|
}: UseApplicationDetailsStageDataParams) {
|
||||||
|
|||||||
@ -298,6 +298,29 @@ html {
|
|||||||
scrollbar-color: #e2e8f0 transparent;
|
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) */
|
/* Extra-thin, light vertical scrollbar (e.g. modals) */
|
||||||
.custom-scrollbar-slim::-webkit-scrollbar {
|
.custom-scrollbar-slim::-webkit-scrollbar {
|
||||||
width: 2px;
|
width: 2px;
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user