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>

View File

@@ -145,6 +145,15 @@ export function SettingsContent({ initialSettings, isSuperAdmin = true }: Settin
'analytics_observer_comparison_tab',
'analytics_pdf_enabled',
'analytics_pdf_sections',
'observer_show_team_tab',
'applicant_show_evaluation_feedback',
'applicant_show_evaluation_scores',
'applicant_show_evaluation_criteria',
'applicant_show_evaluation_text',
'applicant_show_livefinal_feedback',
'applicant_show_livefinal_scores',
'applicant_show_deliberation_feedback',
'applicant_hide_feedback_from_rejected',
])
const auditSecuritySettings = getSettingsByKeys([
@@ -785,6 +794,66 @@ function AnalyticsSettingsSection({ settings }: { settings: Record<string, strin
settingKey="analytics_observer_comparison_tab"
value={settings.analytics_observer_comparison_tab || 'true'}
/>
<SettingToggle
label="Team Tab"
description="Show team members on observer project detail page"
settingKey="observer_show_team_tab"
value={settings.observer_show_team_tab || 'true'}
/>
<div className="border-t pt-4 space-y-3">
<Label className="text-sm font-medium">Applicant Feedback Visibility</Label>
<p className="text-xs text-muted-foreground">
Control what anonymous jury feedback applicants can see on their dashboard
</p>
<SettingToggle
label="Show Evaluation Feedback"
description="Enable anonymous jury evaluation reviews for applicants"
settingKey="applicant_show_evaluation_feedback"
value={settings.applicant_show_evaluation_feedback || 'false'}
/>
<SettingToggle
label="Show Global Scores"
description="Show overall score in evaluation feedback"
settingKey="applicant_show_evaluation_scores"
value={settings.applicant_show_evaluation_scores || 'false'}
/>
<SettingToggle
label="Show Criterion Scores"
description="Show per-criterion breakdown in evaluation feedback"
settingKey="applicant_show_evaluation_criteria"
value={settings.applicant_show_evaluation_criteria || 'false'}
/>
<SettingToggle
label="Show Written Feedback"
description="Show jury's written comments to applicants"
settingKey="applicant_show_evaluation_text"
value={settings.applicant_show_evaluation_text || 'false'}
/>
<SettingToggle
label="Show Live Final Feedback"
description="Show live final jury scores to applicants"
settingKey="applicant_show_livefinal_feedback"
value={settings.applicant_show_livefinal_feedback || 'false'}
/>
<SettingToggle
label="Show Live Final Individual Scores"
description="Show individual jury member scores from live finals"
settingKey="applicant_show_livefinal_scores"
value={settings.applicant_show_livefinal_scores || 'false'}
/>
<SettingToggle
label="Show Deliberation Results"
description="Show deliberation voting results to applicants"
settingKey="applicant_show_deliberation_feedback"
value={settings.applicant_show_deliberation_feedback || 'false'}
/>
<SettingToggle
label="Hide from Rejected Projects"
description="Hide all feedback from projects that have been rejected"
settingKey="applicant_hide_feedback_from_rejected"
value={settings.applicant_hide_feedback_from_rejected || 'false'}
/>
</div>
<div className="border-t pt-4 space-y-3">
<Label className="text-sm font-medium">PDF Reports</Label>
<SettingToggle