feat: add clickable projects and doc counts to finalization page
All checks were successful
Build and Push Docker Image / build (push) Successful in 8m36s
All checks were successful
Build and Push Docker Image / build (push) Successful in 8m36s
Project names now link to their detail page on all finalization tabs. Submission/intake rounds show a docs submitted/required column. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -41,6 +41,7 @@ import {
|
|||||||
Send,
|
Send,
|
||||||
Eye,
|
Eye,
|
||||||
} from 'lucide-react'
|
} from 'lucide-react'
|
||||||
|
import Link from 'next/link'
|
||||||
import { cn } from '@/lib/utils'
|
import { cn } from '@/lib/utils'
|
||||||
import { projectStateConfig } from '@/lib/round-config'
|
import { projectStateConfig } from '@/lib/round-config'
|
||||||
import { EmailPreviewDialog } from './email-preview-dialog'
|
import { EmailPreviewDialog } from './email-preview-dialog'
|
||||||
@@ -204,8 +205,12 @@ export function FinalizationTab({ roundId, roundStatus }: FinalizationTabProps)
|
|||||||
batchUpdate.mutate({ roundId, outcomes })
|
batchUpdate.mutate({ roundId, outcomes })
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Does this round have document requirements to show?
|
||||||
|
const hasDocColumn = (summary?.roundType === 'SUBMISSION' || summary?.roundType === 'INTAKE') &&
|
||||||
|
summary?.projects.some((p) => p.documentsRequired != null && p.documentsRequired > 0)
|
||||||
|
|
||||||
// Column count for colSpan
|
// Column count for colSpan
|
||||||
const colCount = (summary?.isFinalized ? 0 : 1) + 4 + (summary?.roundType === 'EVALUATION' ? 1 : 0) + 1
|
const colCount = (summary?.isFinalized ? 0 : 1) + 4 + (summary?.roundType === 'EVALUATION' ? 1 : 0) + (hasDocColumn ? 1 : 0) + 1
|
||||||
|
|
||||||
// Shared row renderer
|
// Shared row renderer
|
||||||
const renderProjectRow = (project: (typeof filteredProjects)[number]) => (
|
const renderProjectRow = (project: (typeof filteredProjects)[number]) => (
|
||||||
@@ -225,7 +230,9 @@ export function FinalizationTab({ roundId, roundStatus }: FinalizationTabProps)
|
|||||||
</td>
|
</td>
|
||||||
)}
|
)}
|
||||||
<td className="px-3 py-2.5">
|
<td className="px-3 py-2.5">
|
||||||
<div className="font-medium truncate max-w-[200px]">{project.title}</div>
|
<Link href={`/admin/projects/${project.id}`} className="font-medium truncate max-w-[200px] block hover:underline text-primary">
|
||||||
|
{project.title}
|
||||||
|
</Link>
|
||||||
{project.teamName && (
|
{project.teamName && (
|
||||||
<div className="text-xs text-muted-foreground truncate">{project.teamName}</div>
|
<div className="text-xs text-muted-foreground truncate">{project.teamName}</div>
|
||||||
)}
|
)}
|
||||||
@@ -241,6 +248,20 @@ export function FinalizationTab({ roundId, roundStatus }: FinalizationTabProps)
|
|||||||
{project.currentState.replace('_', ' ')}
|
{project.currentState.replace('_', ' ')}
|
||||||
</Badge>
|
</Badge>
|
||||||
</td>
|
</td>
|
||||||
|
{hasDocColumn && (
|
||||||
|
<td className="px-3 py-2.5 text-center hidden lg:table-cell">
|
||||||
|
<span className={cn(
|
||||||
|
'text-sm font-medium',
|
||||||
|
(project.documentsSubmitted ?? 0) >= (project.documentsRequired ?? 0)
|
||||||
|
? 'text-green-700'
|
||||||
|
: (project.documentsSubmitted ?? 0) > 0
|
||||||
|
? 'text-amber-600'
|
||||||
|
: 'text-muted-foreground',
|
||||||
|
)}>
|
||||||
|
{project.documentsSubmitted ?? 0}/{project.documentsRequired ?? 0}
|
||||||
|
</span>
|
||||||
|
</td>
|
||||||
|
)}
|
||||||
{summary?.roundType === 'EVALUATION' && (
|
{summary?.roundType === 'EVALUATION' && (
|
||||||
<td className="px-3 py-2.5 text-center hidden lg:table-cell text-muted-foreground">
|
<td className="px-3 py-2.5 text-center hidden lg:table-cell text-muted-foreground">
|
||||||
{project.evaluationScore != null
|
{project.evaluationScore != null
|
||||||
@@ -555,6 +576,9 @@ export function FinalizationTab({ roundId, roundStatus }: FinalizationTabProps)
|
|||||||
<th className="text-left px-3 py-2.5 font-medium hidden sm:table-cell">Category</th>
|
<th className="text-left px-3 py-2.5 font-medium hidden sm:table-cell">Category</th>
|
||||||
<th className="text-left px-3 py-2.5 font-medium hidden md:table-cell">Country</th>
|
<th className="text-left px-3 py-2.5 font-medium hidden md:table-cell">Country</th>
|
||||||
<th className="text-center px-3 py-2.5 font-medium">Current State</th>
|
<th className="text-center px-3 py-2.5 font-medium">Current State</th>
|
||||||
|
{hasDocColumn && (
|
||||||
|
<th className="text-center px-3 py-2.5 font-medium hidden lg:table-cell">Docs</th>
|
||||||
|
)}
|
||||||
{summary.roundType === 'EVALUATION' && (
|
{summary.roundType === 'EVALUATION' && (
|
||||||
<th className="text-center px-3 py-2.5 font-medium hidden lg:table-cell">Score / Rank</th>
|
<th className="text-center px-3 py-2.5 font-medium hidden lg:table-cell">Score / Rank</th>
|
||||||
)}
|
)}
|
||||||
|
|||||||
@@ -44,6 +44,8 @@ export type FinalizationSummary = {
|
|||||||
proposedOutcome: ProjectRoundStateValue | null
|
proposedOutcome: ProjectRoundStateValue | null
|
||||||
evaluationScore?: number | null
|
evaluationScore?: number | null
|
||||||
rankPosition?: number | null
|
rankPosition?: number | null
|
||||||
|
documentsSubmitted?: number | null
|
||||||
|
documentsRequired?: number | null
|
||||||
}>
|
}>
|
||||||
categoryTargets: {
|
categoryTargets: {
|
||||||
startupTarget: number | null
|
startupTarget: number | null
|
||||||
@@ -487,6 +489,34 @@ export async function getFinalizationSummary(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Get document submission counts for SUBMISSION/INTAKE rounds
|
||||||
|
let documentsRequired: number | null = null
|
||||||
|
let docCountMap = new Map<string, number>()
|
||||||
|
|
||||||
|
if (round.roundType === 'SUBMISSION' || round.roundType === 'INTAKE') {
|
||||||
|
const requirements = await prisma.fileRequirement.findMany({
|
||||||
|
where: { roundId, isRequired: true },
|
||||||
|
select: { id: true },
|
||||||
|
})
|
||||||
|
documentsRequired = requirements.length
|
||||||
|
|
||||||
|
if (documentsRequired > 0) {
|
||||||
|
const projectIds = projectStates.map((prs: any) => prs.project.id)
|
||||||
|
const fileCounts = await prisma.projectFile.groupBy({
|
||||||
|
by: ['projectId'],
|
||||||
|
where: {
|
||||||
|
roundId,
|
||||||
|
projectId: { in: projectIds },
|
||||||
|
requirementId: { in: requirements.map((r) => r.id) },
|
||||||
|
},
|
||||||
|
_count: { _all: true },
|
||||||
|
})
|
||||||
|
for (const fc of fileCounts) {
|
||||||
|
docCountMap.set(fc.projectId, fc._count._all)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Build project list
|
// Build project list
|
||||||
const projects = projectStates.map((prs: any) => ({
|
const projects = projectStates.map((prs: any) => ({
|
||||||
id: prs.project.id,
|
id: prs.project.id,
|
||||||
@@ -498,6 +528,8 @@ export async function getFinalizationSummary(
|
|||||||
proposedOutcome: prs.proposedOutcome as ProjectRoundStateValue | null,
|
proposedOutcome: prs.proposedOutcome as ProjectRoundStateValue | null,
|
||||||
evaluationScore: scoreMap.get(prs.project.id) ?? null,
|
evaluationScore: scoreMap.get(prs.project.id) ?? null,
|
||||||
rankPosition: rankMap.get(prs.project.id) ?? null,
|
rankPosition: rankMap.get(prs.project.id) ?? null,
|
||||||
|
documentsSubmitted: documentsRequired != null ? (docCountMap.get(prs.project.id) ?? 0) : null,
|
||||||
|
documentsRequired,
|
||||||
}))
|
}))
|
||||||
|
|
||||||
// Category target progress
|
// Category target progress
|
||||||
|
|||||||
Reference in New Issue
Block a user