Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 7 additions & 0 deletions src/apps/review/src/lib/hooks/useFetchScreeningReview.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1993,12 +1993,19 @@ export function useFetchScreeningReview(): useFetchScreeningReviewProps {
// get review progress from challenge review
const reviewProgress = useMemo(() => calculateReviewProgress({
challengePhases: challengeInfo?.phases,
checkpointReviewRows: checkpointReview,
checkpointScreeningRows: checkpoint,
currentPhaseName: challengeInfo?.currentPhaseObject?.name || challengeInfo?.currentPhase,
isDesignChallenge: challengeInfo?.track?.name === DESIGN,
reviewRows: review,
screeningRows: screening,
}), [
challengeInfo?.phases,
challengeInfo?.currentPhase,
challengeInfo?.currentPhaseObject?.name,
challengeInfo?.track?.name,
checkpoint,
checkpointReview,
review,
screening,
])
Expand Down
82 changes: 82 additions & 0 deletions src/apps/review/src/lib/utils/reviewProgress.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -53,13 +53,15 @@ const createReviewSubmission = (
const createScreeningRow = (
submissionId: string,
result: Screening['result'],
overrides: Partial<Screening> = {},
): Screening => ({
challengeId: 'challenge-id',
createdAt: '2025-01-01T00:00:00.000Z',
memberId: `member-${submissionId}`,
result,
score: '100',
submissionId,
...overrides,
})

describe('calculateReviewProgress', () => {
Expand All @@ -80,6 +82,9 @@ describe('calculateReviewProgress', () => {

const progress = calculateReviewProgress({
challengePhases: reviewPhases,
checkpointReviewRows: [],
checkpointScreeningRows: [],
currentPhaseName: 'Review',
isDesignChallenge: false,
reviewRows,
screeningRows,
Expand All @@ -101,6 +106,9 @@ describe('calculateReviewProgress', () => {

const progress = calculateReviewProgress({
challengePhases: reviewPhases,
checkpointReviewRows: [],
checkpointScreeningRows: [],
currentPhaseName: 'Review',
isDesignChallenge: false,
reviewRows,
screeningRows,
Expand All @@ -122,6 +130,9 @@ describe('calculateReviewProgress', () => {

const progress = calculateReviewProgress({
challengePhases: reviewPhases,
checkpointReviewRows: [],
checkpointScreeningRows: [],
currentPhaseName: 'Review',
isDesignChallenge: true,
reviewRows,
screeningRows,
Expand All @@ -130,4 +141,75 @@ describe('calculateReviewProgress', () => {
expect(progress)
.toBe(50)
})

it('uses completed checkpoint review rows for an open checkpoint review phase', () => {
const checkpointReviewPhase = createPhase('Checkpoint Review')
const checkpointReviewRows: Screening[] = [
createScreeningRow('submission-one', 'PASS', { reviewStatus: 'COMPLETED' }),
createScreeningRow('submission-two', 'PASS', { reviewStatus: 'SUBMITTED' }),
]

const progress = calculateReviewProgress({
challengePhases: [checkpointReviewPhase],
checkpointReviewRows,
checkpointScreeningRows: [],
currentPhaseName: '',
isDesignChallenge: true,
reviewRows: [createReviewSubmission('final-review', 'PENDING')],
screeningRows: [],
})

expect(progress)
.toBe(100)
})

it('uses checkpoint screening rows for partial checkpoint screening progress', () => {
const progress = calculateReviewProgress({
challengePhases: [createPhase('Checkpoint Screening')],
checkpointReviewRows: [],
checkpointScreeningRows: [
createScreeningRow('submission-one', 'PASS', { reviewStatus: 'COMPLETED' }),
createScreeningRow('submission-two', '-', { reviewStatus: 'IN_PROGRESS' }),
],
currentPhaseName: 'Checkpoint Screening',
isDesignChallenge: true,
reviewRows: [],
screeningRows: [],
})

expect(progress)
.toBe(50)
})

it('counts each reviewer assignment in multi-screener phase progress', () => {
const screeningRows: Screening[] = [
createScreeningRow('submission-one', '-', {
screeningReviews: [
{
result: 'PASS',
reviewStatus: 'COMPLETED',
score: '100',
},
{
result: '-',
reviewStatus: 'PENDING',
score: 'Pending',
},
],
}),
]

const progress = calculateReviewProgress({
challengePhases: [createPhase('Screening')],
checkpointReviewRows: [],
checkpointScreeningRows: [],
currentPhaseName: 'Screening',
isDesignChallenge: false,
reviewRows: [],
screeningRows,
})

expect(progress)
.toBe(50)
})
})
103 changes: 101 additions & 2 deletions src/apps/review/src/lib/utils/reviewProgress.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,10 +8,87 @@ import type {

import { shouldIncludeInReviewPhase } from './reviewPhaseGuards'

const COMPLETED_REVIEW_STATUSES = new Set(['COMPLETED', 'SUBMITTED'])

const REVIEW_PROGRESS_PHASES = new Set([
'checkpointreview',
'checkpointscreening',
'review',
'screening',
])

const normalizeScreeningResult = (result?: string | null): string => (result ?? '')
.trim()
.toUpperCase()

/**
* Normalizes a phase name for phase-aware progress comparisons.
*
* @param phaseName - Human-readable challenge phase name.
* @returns Lower-case alphabetic phase key, or an empty string when absent.
* Used internally by review progress phase selection. This function does not throw.
*/
const normalizeProgressPhaseName = (phaseName?: string | null): string => (phaseName ?? '')
.trim()
.toLowerCase()
.replace(/[^a-z]/g, '')

/**
* Resolves the review-like phase whose rows should drive the progress bar.
*
* @param currentPhaseName - Current phase reported by the challenge response.
* @param challengePhases - Challenge phase metadata used when the current phase is absent.
* @returns Normalized supported phase name, or `undefined` when no review-like phase is active.
* Used by `calculateReviewProgress` and does not throw.
*/
const resolveReviewProgressPhase = (
currentPhaseName: string | undefined,
challengePhases?: BackendPhase[],
): string | undefined => {
const normalizedCurrentPhase = normalizeProgressPhaseName(currentPhaseName)
if (REVIEW_PROGRESS_PHASES.has(normalizedCurrentPhase)) {
return normalizedCurrentPhase
}

const openReviewPhase = (challengePhases ?? []).find(phase => (
phase.isOpen
&& REVIEW_PROGRESS_PHASES.has(normalizeProgressPhaseName(phase.name))
))

return openReviewPhase
? normalizeProgressPhaseName(openReviewPhase.name)
: undefined
}

/**
* Calculates completion across screening-style phase rows.
*
* @param rows - Screening, Checkpoint Screening, or Checkpoint Review rows.
* @returns Rounded completion percentage in the inclusive range [0, 100].
* Multi-screener rows count each assignment. Rows without assignments remain pending.
* Used by `calculateReviewProgress` and does not throw.
*/
const calculateScreeningRowsProgress = (rows: Screening[]): number => {
let completedReviewCount = 0
let totalReviewCount = 0

rows.forEach(row => {
const reviewStatuses = row.screeningReviews?.length
? row.screeningReviews.map(review => review.reviewStatus)
: [row.reviewStatus ?? row.myReviewStatus]

totalReviewCount += reviewStatuses.length
completedReviewCount += reviewStatuses.filter(status => (
COMPLETED_REVIEW_STATUSES.has((status ?? '').trim()
.toUpperCase())
)).length
})

return totalReviewCount
? Math.round((completedReviewCount * 100) / totalReviewCount)
: 0
}

const resolveReviewSubmissionIds = (submission: SubmissionInfo): string[] => {
const candidateIds = new Set<string>()
const submissionId = submission.id?.trim()
Expand Down Expand Up @@ -78,24 +155,46 @@ const isCompletedReviewSubmission = (submission: SubmissionInfo): boolean => {

type CalculateReviewProgressArgs = {
challengePhases?: BackendPhase[]
checkpointReviewRows: Screening[]
checkpointScreeningRows: Screening[]
currentPhaseName: string | undefined
isDesignChallenge: boolean
reviewRows: SubmissionInfo[]
screeningRows: Screening[]
}

/**
* Calculates review phase completion progress as a percentage.
* Screening-failed submissions are excluded whenever screening outcomes are available.
* Calculates completion progress for the current review-like challenge phase.
* Final Review preserves screening outcome and submission-history filtering, while
* Screening and checkpoint phases use their phase-specific assignment rows.
*
* @param args - Inputs needed to evaluate review progress.
* @returns Rounded completion percentage in the inclusive range [0, 100].
* Used by `useFetchScreeningReview` for the challenge header. This function does not throw.
*/
export const calculateReviewProgress = ({
challengePhases,
checkpointReviewRows,
checkpointScreeningRows,
currentPhaseName,
isDesignChallenge,
reviewRows,
screeningRows,
}: CalculateReviewProgressArgs): number => {
const progressPhase = resolveReviewProgressPhase(currentPhaseName, challengePhases)

if (progressPhase === 'checkpointreview') {
return calculateScreeningRowsProgress(checkpointReviewRows)
}

if (progressPhase === 'checkpointscreening') {
return calculateScreeningRowsProgress(checkpointScreeningRows)
}

if (progressPhase === 'screening') {
return calculateScreeningRowsProgress(screeningRows)
}

if (!reviewRows.length) {
return 0
}
Expand Down
Loading