feat: observer team tab, admin-controlled applicant feedback visibility
All checks were successful
Build and Push Docker Image / build (push) Successful in 9m13s

- Add Team tab to observer project detail (configurable via admin settings)
- Move applicant jury feedback visibility from per-round config to admin settings
- Add per-round-type controls: evaluation, live final, deliberation
- Support anonymous LiveVote and DeliberationVote display for applicants
- Add fine-grained toggles: scores, criteria, written feedback, hide from rejected
- Backwards compatible: falls back to old per-round config if admin settings not set
- New admin settings section under Analytics tab with all visibility controls
- Seed new SystemSettings keys for observer/applicant visibility

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-03-05 16:50:20 +01:00
parent 6b6f5e33f5
commit 94814bd505
6 changed files with 477 additions and 168 deletions

View File

@@ -47,6 +47,7 @@ export function ObserverProjectDetail({ projectId }: { projectId: string }) {
{ id: projectId },
{ refetchInterval: 30_000 },
)
const { data: flags } = trpc.settings.getFeatureFlags.useQuery()
const roundId = data?.assignments?.[0]?.roundId as string | undefined
const { data: activeForm } = trpc.evaluation.getStageForm.useQuery(
@@ -242,6 +243,14 @@ export function ObserverProjectDetail({ projectId }: { projectId: string }) {
)}
</TabsTrigger>
<TabsTrigger value="files">Files</TabsTrigger>
{flags?.observerShowTeamTab && project.teamMembers.length > 0 && (
<TabsTrigger value="team">
Team
<Badge variant="secondary" className="ml-1.5 h-4 px-1 text-xs">
{project.teamMembers.length}
</Badge>
</TabsTrigger>
)}
</TabsList>
{/* ── Overview Tab ── */}
@@ -854,6 +863,48 @@ export function ObserverProjectDetail({ projectId }: { projectId: string }) {
)}
</TabsContent>
{/* ── Team Tab ── */}
{flags?.observerShowTeamTab && project.teamMembers.length > 0 && (
<TabsContent value="team" className="mt-6">
<AnimatedCard index={0}>
<Card>
<CardHeader>
<CardTitle className="flex items-center gap-2.5 text-lg">
<div className="rounded-lg bg-indigo-500/10 p-1.5">
<Users className="h-4 w-4 text-indigo-500" />
</div>
Team Members
</CardTitle>
</CardHeader>
<CardContent>
<div className="space-y-3">
{project.teamMembers.map((member) => (
<div key={member.userId} className="flex items-center gap-3 rounded-lg border p-3">
<UserAvatar
user={member.user}
avatarUrl={(member.user as { avatarUrl?: string | null }).avatarUrl}
size="md"
/>
<div className="min-w-0 flex-1">
<p className="text-sm font-semibold truncate">
{member.user.name || 'Unnamed'}
</p>
<p className="text-xs text-muted-foreground truncate">
{member.user.email}
</p>
</div>
<Badge variant="outline" className="shrink-0 text-xs">
{member.role === 'LEAD' ? 'Lead' : member.role === 'ADVISOR' ? 'Advisor' : 'Member'}
</Badge>
</div>
))}
</div>
</CardContent>
</Card>
</AnimatedCard>
</TabsContent>
)}
{/* ── Files Tab ── */}
<TabsContent value="files" className="mt-6">
<Card>