feat: resolve observer project page round default and add selector
The observer full project page used to call getProjectDetail without a round, getting cross-round contaminated stats. It now resolves a default — the currently OPEN round the project is in, falling back to the most recently CLOSED one — and renders a selector chip in the score card whenever the project participated in more than one candidate round. Initial selection respects the ?round= query param. A new observer procedure (getProjectRoundsForObserver) returns the project's open or closed rounds for the picker. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -6,10 +6,13 @@ export const dynamic = 'force-dynamic'
|
|||||||
|
|
||||||
export default async function ObserverProjectDetailPage({
|
export default async function ObserverProjectDetailPage({
|
||||||
params,
|
params,
|
||||||
|
searchParams,
|
||||||
}: {
|
}: {
|
||||||
params: Promise<{ projectId: string }>
|
params: Promise<{ projectId: string }>
|
||||||
|
searchParams: Promise<{ round?: string }>
|
||||||
}) {
|
}) {
|
||||||
const { projectId } = await params
|
const { projectId } = await params
|
||||||
|
const sp = await searchParams
|
||||||
|
|
||||||
return <ObserverProjectDetail projectId={projectId} />
|
return <ObserverProjectDetail projectId={projectId} initialRoundId={sp.round} />
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
'use client'
|
'use client'
|
||||||
|
|
||||||
|
import { useEffect, useState } from 'react'
|
||||||
import Link from 'next/link'
|
import Link from 'next/link'
|
||||||
import type { Route } from 'next'
|
import type { Route } from 'next'
|
||||||
import { useRouter } from 'next/navigation'
|
import { useRouter } from 'next/navigation'
|
||||||
@@ -44,15 +45,41 @@ import {
|
|||||||
} from 'lucide-react'
|
} from 'lucide-react'
|
||||||
import { cn, formatDate, formatDateOnly } from '@/lib/utils'
|
import { cn, formatDate, formatDateOnly } from '@/lib/utils'
|
||||||
|
|
||||||
export function ObserverProjectDetail({ projectId }: { projectId: string }) {
|
export function ObserverProjectDetail({
|
||||||
|
projectId,
|
||||||
|
initialRoundId,
|
||||||
|
}: {
|
||||||
|
projectId: string
|
||||||
|
initialRoundId?: string
|
||||||
|
}) {
|
||||||
const router = useRouter()
|
const router = useRouter()
|
||||||
|
const [activeRoundId, setActiveRoundId] = useState<string | undefined>(initialRoundId)
|
||||||
|
|
||||||
|
// Resolve a default round when none is set: prefer the currently OPEN round
|
||||||
|
// the project participates in, fall back to the most recently CLOSED one.
|
||||||
|
const { data: roundCandidates } = trpc.analytics.getProjectRoundsForObserver.useQuery(
|
||||||
|
{ projectId },
|
||||||
|
)
|
||||||
|
useEffect(() => {
|
||||||
|
if (activeRoundId || !roundCandidates) return
|
||||||
|
const active = roundCandidates.find((r) => r.status === 'ROUND_ACTIVE')
|
||||||
|
if (active) {
|
||||||
|
setActiveRoundId(active.id)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
const closed = [...roundCandidates]
|
||||||
|
.filter((r) => r.status === 'ROUND_CLOSED')
|
||||||
|
.sort((a, b) => b.sortOrder - a.sortOrder)[0]
|
||||||
|
if (closed) setActiveRoundId(closed.id)
|
||||||
|
}, [roundCandidates, activeRoundId])
|
||||||
|
|
||||||
const { data, isLoading } = trpc.analytics.getProjectDetail.useQuery(
|
const { data, isLoading } = trpc.analytics.getProjectDetail.useQuery(
|
||||||
{ id: projectId },
|
{ id: projectId, roundId: activeRoundId },
|
||||||
{ refetchInterval: 30_000 },
|
{ refetchInterval: 30_000 },
|
||||||
)
|
)
|
||||||
const { data: flags } = trpc.settings.getFeatureFlags.useQuery()
|
const { data: flags } = trpc.settings.getFeatureFlags.useQuery()
|
||||||
|
|
||||||
const roundId = data?.assignments?.[0]?.roundId as string | undefined
|
const roundId = activeRoundId ?? (data?.assignments?.[0]?.roundId as string | undefined)
|
||||||
const { data: activeForm } = trpc.evaluation.getStageForm.useQuery(
|
const { data: activeForm } = trpc.evaluation.getStageForm.useQuery(
|
||||||
{ roundId: roundId ?? '', category: data?.project?.competitionCategory },
|
{ roundId: roundId ?? '', category: data?.project?.competitionCategory },
|
||||||
{ enabled: !!roundId },
|
{ enabled: !!roundId },
|
||||||
@@ -223,6 +250,19 @@ export function ObserverProjectDetail({ projectId }: { projectId: string }) {
|
|||||||
{stats.yesPercentage.toFixed(0)}% recommended
|
{stats.yesPercentage.toFixed(0)}% recommended
|
||||||
</p>
|
</p>
|
||||||
)}
|
)}
|
||||||
|
{roundCandidates && roundCandidates.length > 1 && (
|
||||||
|
<div className="mt-3 w-full">
|
||||||
|
<select
|
||||||
|
className="w-full rounded border bg-background px-2 py-1 text-xs"
|
||||||
|
value={activeRoundId ?? ''}
|
||||||
|
onChange={(e) => setActiveRoundId(e.target.value)}
|
||||||
|
>
|
||||||
|
{roundCandidates.map((r) => (
|
||||||
|
<option key={r.id} value={r.id}>{r.name}</option>
|
||||||
|
))}
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
</CardContent>
|
</CardContent>
|
||||||
</Card>
|
</Card>
|
||||||
|
|||||||
@@ -2187,6 +2187,26 @@ export const analyticsRouter = router({
|
|||||||
}
|
}
|
||||||
}),
|
}),
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns rounds the project has participated in, restricted to those that
|
||||||
|
* are open or already closed. Used by the observer full project page to
|
||||||
|
* resolve a default round when none is specified in the URL.
|
||||||
|
*/
|
||||||
|
getProjectRoundsForObserver: observerProcedure
|
||||||
|
.input(z.object({ projectId: z.string() }))
|
||||||
|
.query(async ({ ctx, input }) => {
|
||||||
|
const states = await ctx.prisma.projectRoundState.findMany({
|
||||||
|
where: { projectId: input.projectId },
|
||||||
|
select: {
|
||||||
|
round: { select: { id: true, name: true, status: true, sortOrder: true } },
|
||||||
|
},
|
||||||
|
})
|
||||||
|
return states
|
||||||
|
.map((s) => s.round)
|
||||||
|
.filter((r) => r.status === 'ROUND_ACTIVE' || r.status === 'ROUND_CLOSED')
|
||||||
|
.sort((a, b) => a.sortOrder - b.sortOrder)
|
||||||
|
}),
|
||||||
|
|
||||||
getRecentFiles: observerProcedure
|
getRecentFiles: observerProcedure
|
||||||
.input(z.object({ roundId: z.string(), limit: z.number().min(1).max(50).default(10) }))
|
.input(z.object({ roundId: z.string(), limit: z.number().min(1).max(50).default(10) }))
|
||||||
.query(async ({ ctx, input }) => {
|
.query(async ({ ctx, input }) => {
|
||||||
|
|||||||
Reference in New Issue
Block a user