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;