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

@@ -25,7 +25,7 @@ function categorizeModel(modelId: string): string {
return 'other'
}
function inferSettingCategory(key: string): 'AI' | 'BRANDING' | 'EMAIL' | 'STORAGE' | 'SECURITY' | 'DEFAULTS' | 'WHATSAPP' | 'FEATURE_FLAGS' {
function inferSettingCategory(key: string): 'AI' | 'BRANDING' | 'EMAIL' | 'STORAGE' | 'SECURITY' | 'DEFAULTS' | 'WHATSAPP' | 'FEATURE_FLAGS' | 'ANALYTICS' {
if (key.startsWith('openai') || key.startsWith('ai_') || key.startsWith('anthropic')) return 'AI'
if (key.startsWith('smtp_') || key.startsWith('email_')) return 'EMAIL'
if (key.startsWith('storage_') || key.startsWith('local_storage') || key.startsWith('max_file') || key.startsWith('avatar_') || key.startsWith('allowed_file')) return 'STORAGE'
@@ -33,6 +33,7 @@ function inferSettingCategory(key: string): 'AI' | 'BRANDING' | 'EMAIL' | 'STORA
if (key.startsWith('whatsapp_')) return 'WHATSAPP'
if (key.startsWith('security_') || key.startsWith('session_')) return 'SECURITY'
if (key.startsWith('learning_hub_') || key.startsWith('jury_compare_') || key.startsWith('support_')) return 'FEATURE_FLAGS'
if (key.startsWith('applicant_') || key.startsWith('observer_') || key.startsWith('analytics_')) return 'ANALYTICS'
return 'DEFAULTS'
}
@@ -42,34 +43,40 @@ export const settingsRouter = router({
* These are non-sensitive settings that can be exposed to any user
*/
getFeatureFlags: protectedProcedure.query(async ({ ctx }) => {
const [whatsappEnabled, juryCompareEnabled, learningHubExternal, learningHubExternalUrl, supportEmail, accountReminderDays] = await Promise.all([
ctx.prisma.systemSettings.findUnique({
where: { key: 'whatsapp_enabled' },
}),
ctx.prisma.systemSettings.findUnique({
where: { key: 'jury_compare_enabled' },
}),
ctx.prisma.systemSettings.findUnique({
where: { key: 'learning_hub_external' },
}),
ctx.prisma.systemSettings.findUnique({
where: { key: 'learning_hub_external_url' },
}),
ctx.prisma.systemSettings.findUnique({
where: { key: 'support_email' },
}),
ctx.prisma.systemSettings.findUnique({
where: { key: 'account_reminder_days' },
}),
])
const keys = [
'whatsapp_enabled', 'jury_compare_enabled', 'learning_hub_external',
'learning_hub_external_url', 'support_email', 'account_reminder_days',
'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 settings = await ctx.prisma.systemSettings.findMany({
where: { key: { in: keys } },
})
const map = new Map(settings.map((s) => [s.key, s.value]))
const flag = (k: string, def = 'false') => (map.get(k) ?? def) === 'true'
return {
whatsappEnabled: whatsappEnabled?.value === 'true',
juryCompareEnabled: juryCompareEnabled?.value === 'true',
learningHubExternal: learningHubExternal?.value === 'true',
learningHubExternalUrl: learningHubExternalUrl?.value || '',
supportEmail: supportEmail?.value || '',
accountReminderDays: parseInt(accountReminderDays?.value || '3', 10),
whatsappEnabled: flag('whatsapp_enabled'),
juryCompareEnabled: flag('jury_compare_enabled'),
learningHubExternal: flag('learning_hub_external'),
learningHubExternalUrl: map.get('learning_hub_external_url') || '',
supportEmail: map.get('support_email') || '',
accountReminderDays: parseInt(map.get('account_reminder_days') || '3', 10),
observerShowTeamTab: flag('observer_show_team_tab', 'true'),
applicantFeedback: {
evaluationEnabled: flag('applicant_show_evaluation_feedback'),
evaluationShowScores: flag('applicant_show_evaluation_scores'),
evaluationShowCriteria: flag('applicant_show_evaluation_criteria'),
evaluationShowText: flag('applicant_show_evaluation_text'),
liveFinalEnabled: flag('applicant_show_livefinal_feedback'),
liveFinalShowScores: flag('applicant_show_livefinal_scores'),
deliberationEnabled: flag('applicant_show_deliberation_feedback'),
hideFromRejected: flag('applicant_hide_feedback_from_rejected'),
},
}
}),