diff --git a/src/app/(admin)/admin/projects/[id]/page.tsx b/src/app/(admin)/admin/projects/[id]/page.tsx index 5ffbc75..e8331c9 100644 --- a/src/app/(admin)/admin/projects/[id]/page.tsx +++ b/src/app/(admin)/admin/projects/[id]/page.tsx @@ -43,9 +43,6 @@ import { Users, FileText, Calendar, - CheckCircle2, - XCircle, - Circle, Clock, BarChart3, ThumbsUp, @@ -562,105 +559,9 @@ function ProjectDetailContent({ projectId }: { projectId: string }) { - {/* Requirements organized by round */} - {competitionRounds.length > 0 && allRequirements.length > 0 ? ( - <> - {competitionRounds.map((round: { id: string; name: string }) => { - const roundRequirements = allRequirements.filter((req: any) => req.roundId === round.id) - if (roundRequirements.length === 0) return null - - return ( -
-
-

{round.name}

- - {roundRequirements.length} requirement{roundRequirements.length !== 1 ? 's' : ''} - -
-
- {roundRequirements.map((req: any) => { - // Find file that fulfills this requirement - const fulfilledFile = files?.find((f: any) => f.requirementId === req.id) - const isFulfilled = !!fulfilledFile - - return ( -
-
- {isFulfilled ? ( - - ) : ( - - )} -
-
-

{req.name}

- {req.isRequired && ( - - Required - - )} -
- {req.description && ( -

- {req.description} -

- )} -
- {req.acceptedMimeTypes?.length > 0 && ( - - {req.acceptedMimeTypes.map((mime: string) => { - if (mime === 'application/pdf') return 'PDF' - if (mime === 'image/*') return 'Images' - if (mime === 'video/*') return 'Video' - if (mime.includes('wordprocessing')) return 'Word' - if (mime.includes('spreadsheet')) return 'Excel' - if (mime.includes('presentation')) return 'PowerPoint' - return mime.split('/')[1] || mime - }).join(', ')} - - )} - {req.maxSizeMB && ( - • Max {req.maxSizeMB}MB - )} -
- {isFulfilled && fulfilledFile && ( -

- ✓ {fulfilledFile.fileName} -

- )} -
-
- {!isFulfilled && ( - - Missing - - )} -
- ) - })} -
-
- ) - })} - - - ) : null} - - {/* General file upload section */} + {/* File upload */}
-

- {allRequirements.length > 0 ? 'Additional Documents' : 'Upload Files'} -

-

- Upload files not tied to specific requirements -

+

Upload Files

({ id: r.id, name: r.name }))} @@ -674,33 +575,30 @@ function ProjectDetailContent({ projectId }: { projectId: string }) { {files && files.length > 0 && ( <> -
-

All Uploaded Files

- ({ - id: f.id, - fileName: f.fileName, - fileType: f.fileType, - mimeType: f.mimeType, - size: f.size, - bucket: f.bucket, - objectKey: f.objectKey, - pageCount: f.pageCount, - textPreview: f.textPreview, - detectedLang: f.detectedLang, - langConfidence: f.langConfidence, - analyzedAt: f.analyzedAt ? String(f.analyzedAt) : null, - requirementId: f.requirementId, - requirement: f.requirement ? { - id: f.requirement.id, - name: f.requirement.name, - description: f.requirement.description, - isRequired: f.requirement.isRequired, - } : null, - }))} - /> -
+ ({ + id: f.id, + fileName: f.fileName, + fileType: f.fileType, + mimeType: f.mimeType, + size: f.size, + bucket: f.bucket, + objectKey: f.objectKey, + pageCount: f.pageCount, + textPreview: f.textPreview, + detectedLang: f.detectedLang, + langConfidence: f.langConfidence, + analyzedAt: f.analyzedAt ? String(f.analyzedAt) : null, + requirementId: f.requirementId, + requirement: f.requirement ? { + id: f.requirement.id, + name: f.requirement.name, + description: f.requirement.description, + isRequired: f.requirement.isRequired, + } : null, + }))} + /> )} diff --git a/src/components/observer/observer-project-detail.tsx b/src/components/observer/observer-project-detail.tsx index 211dfc9..0b3e03d 100644 --- a/src/components/observer/observer-project-detail.tsx +++ b/src/components/observer/observer-project-detail.tsx @@ -38,6 +38,7 @@ import { Heart, Clock, MessageSquare, + ArrowLeft, } from 'lucide-react' import { cn, formatDate, formatDateOnly } from '@/lib/utils' @@ -85,7 +86,7 @@ export function ObserverProjectDetail({ projectId }: { projectId: string }) { ) } - const { project, assignments, stats, competitionRounds, projectRoundStates, allRequirements, filteringResult } = + const { project, assignments, stats, competitionRounds, projectRoundStates, filteringResult } = data const roundStateMap = new Map( @@ -149,23 +150,13 @@ export function ObserverProjectDetail({ projectId }: { projectId: string }) { return (
- {/* Breadcrumb */} - + {/* Project Header */}
@@ -416,29 +407,6 @@ export function ObserverProjectDetail({ projectId }: { projectId: string }) { )}
- {/* Submission URLs */} - {(project.phase1SubmissionUrl || project.phase2SubmissionUrl) && ( -
-

Submission Links

-
- {project.phase1SubmissionUrl && ( - - )} - {project.phase2SubmissionUrl && ( - - )} -
-
- )} - {/* AI-Assigned Expertise Tags */} {project.projectTags && project.projectTags.length > 0 && (
@@ -518,10 +486,16 @@ export function ObserverProjectDetail({ projectId }: { projectId: string }) { {/* Round History */} {competitionRounds.length > 0 && (() => { - const passedCount = competitionRounds.filter((r) => { - const s = roundStateMap.get(r.id) - return s && (s.state === 'PASSED' || s.state === 'COMPLETED') - }).length + // Find the furthest round index where the project is active or beyond + // Any round before this must have been passed + let furthestActiveIdx = -1 + for (let i = competitionRounds.length - 1; i >= 0; i--) { + const s = roundStateMap.get(competitionRounds[i].id) + if (s && (s.state === 'IN_PROGRESS' || s.state === 'PASSED' || s.state === 'COMPLETED')) { + furthestActiveIdx = i + break + } + } // Find the rejection round — either explicit REJECTED state or inferred const isProjectRejected = project.status === 'REJECTED' @@ -534,17 +508,14 @@ export function ObserverProjectDetail({ projectId }: { projectId: string }) { // infer the rejection round as the furthest round the project reached let inferredRejectionRoundId: string | null = null if (isProjectRejected && !explicitRejectedRound) { - // Find the last round that has any state record (furthest the project got) for (let i = competitionRounds.length - 1; i >= 0; i--) { const s = roundStateMap.get(competitionRounds[i].id) if (s) { - // If it's PASSED/COMPLETED, rejection happened at the next round if (s.state === 'PASSED' || s.state === 'COMPLETED') { if (i + 1 < competitionRounds.length) { inferredRejectionRoundId = competitionRounds[i + 1].id } } else { - // PENDING/IN_PROGRESS in this round means rejected here inferredRejectionRoundId = competitionRounds[i].id } break @@ -557,11 +528,28 @@ export function ObserverProjectDetail({ projectId }: { projectId: string }) { ? competitionRounds.find((r) => r.id === inferredRejectionRoundId) : null) - // Determine which rounds are "not reached" (after rejection point) const rejectedRoundIdx = rejectedRound ? competitionRounds.findIndex((r) => r.id === rejectedRound.id) : -1 + // Compute effective states for all rounds + const effectiveStates = competitionRounds.map((round, idx) => { + const rawState = roundStateMap.get(round.id)?.state + const isRejectionRound = round.id === rejectedRound?.id + const isNotReached = rejectedRoundIdx >= 0 && idx > rejectedRoundIdx + + if (isRejectionRound && !explicitRejectedRound) return 'REJECTED' + if (isNotReached) return 'NOT_REACHED' + // If this round is before the furthest active round and not already PASSED/COMPLETED, + // the project must have passed it to reach the later round + if (furthestActiveIdx > idx && (!rawState || rawState === 'PENDING')) return 'PASSED' + return rawState + }) + + const passedCount = effectiveStates.filter( + (s) => s === 'PASSED' || s === 'COMPLETED', + ).length + return ( @@ -581,17 +569,7 @@ export function ObserverProjectDetail({ projectId }: { projectId: string }) {
    {competitionRounds.map((round, idx) => { - const roundState = roundStateMap.get(round.id) - const rawState = roundState?.state - - // Override state for inferred rejection - const isRejectionRound = round.id === rejectedRound?.id - const isNotReached = rejectedRoundIdx >= 0 && idx > rejectedRoundIdx - const effectiveState = isRejectionRound && !explicitRejectedRound - ? 'REJECTED' - : isNotReached - ? 'NOT_REACHED' - : rawState + const effectiveState = effectiveStates[idx] const roundAssignments = assignments.filter( (a) => a.roundId === round.id, @@ -884,169 +862,44 @@ export function ObserverProjectDetail({ projectId }: { projectId: string }) {
    - Files + Project Files - - Project documents organized by competition round - - - {competitionRounds.length > 0 && allRequirements.length > 0 ? ( - <> - {competitionRounds.map((round) => { - const roundRequirements = allRequirements.filter( - (req) => req.roundId === round.id, - ) - if (roundRequirements.length === 0) return null - - return ( -
    -
    -

    - {round.name} -

    - - {roundRequirements.length} requirement - {roundRequirements.length !== 1 ? 's' : ''} - -
    -
    - {roundRequirements.map((req) => { - const fulfilledFile = project.files?.find( - (f) => f.requirementId === req.id, - ) - const isFulfilled = !!fulfilledFile - - return ( -
    -
    - {isFulfilled ? ( - - ) : ( - - )} -
    -
    -

    - {req.name} -

    - {req.isRequired && ( - - Required - - )} -
    - {req.description && ( -

    - {req.description} -

    - )} -
    - {req.acceptedMimeTypes?.length > 0 && ( - - {req.acceptedMimeTypes - .map((mime) => { - if (mime === 'application/pdf') - return 'PDF' - if (mime === 'image/*') - return 'Images' - if (mime === 'video/*') - return 'Video' - if ( - mime.includes('wordprocessing') - ) - return 'Word' - if (mime.includes('spreadsheet')) - return 'Excel' - if ( - mime.includes('presentation') - ) - return 'PowerPoint' - return mime.split('/')[1] || mime - }) - .join(', ')} - - )} - {req.maxSizeMB && ( - - Max {req.maxSizeMB}MB - - )} -
    - {isFulfilled && fulfilledFile && ( -

    - {fulfilledFile.fileName} -

    - )} -
    -
    - {!isFulfilled && ( - - Missing - - )} -
    - ) - })} -
    -
    - ) - })} - - - ) : null} - + {project.files && project.files.length > 0 ? ( -
    -

    - {allRequirements.length > 0 - ? 'All Uploaded Files' - : 'Uploaded Files'} -

    - ({ - id: f.id, - fileName: f.fileName, - fileType: f.fileType as - | 'EXEC_SUMMARY' - | 'PRESENTATION' - | 'VIDEO' - | 'OTHER' - | 'BUSINESS_PLAN' - | 'VIDEO_PITCH' - | 'SUPPORTING_DOC', - mimeType: f.mimeType, - size: f.size, - bucket: f.bucket, - objectKey: f.objectKey, - pageCount: f.pageCount, - textPreview: f.textPreview, - detectedLang: f.detectedLang, - langConfidence: f.langConfidence, - analyzedAt: f.analyzedAt ? String(f.analyzedAt) : null, - requirementId: f.requirementId, - requirement: f.requirement - ? { - id: f.requirement.id, - name: f.requirement.name, - description: f.requirement.description, - isRequired: f.requirement.isRequired, - } - : null, - }))} - /> -
    + ({ + id: f.id, + fileName: f.fileName, + fileType: f.fileType as + | 'EXEC_SUMMARY' + | 'PRESENTATION' + | 'VIDEO' + | 'OTHER' + | 'BUSINESS_PLAN' + | 'VIDEO_PITCH' + | 'SUPPORTING_DOC', + mimeType: f.mimeType, + size: f.size, + bucket: f.bucket, + objectKey: f.objectKey, + pageCount: f.pageCount, + textPreview: f.textPreview, + detectedLang: f.detectedLang, + langConfidence: f.langConfidence, + analyzedAt: f.analyzedAt ? String(f.analyzedAt) : null, + requirementId: f.requirementId, + requirement: f.requirement + ? { + id: f.requirement.id, + name: f.requirement.name, + description: f.requirement.description, + isRequired: f.requirement.isRequired, + } + : null, + }))} + /> ) : (