feat: external Learning Hub toggle + applicant help button
All checks were successful
Build and Push Docker Image / build (push) Successful in 8m52s
All checks were successful
Build and Push Docker Image / build (push) Successful in 8m52s
- Add admin settings: learning_hub_external, learning_hub_external_url, support_email - Jury/Mentor nav respects external Learning Hub URL (opens in new tab) - RoleNav supports external nav items with ExternalLink icon - Applicant header shows Help button with configurable support email - Settings update mutation now upserts (creates on first use) - Shared inferSettingCategory for consistent category assignment Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -25,24 +25,47 @@ function categorizeModel(modelId: string): string {
|
||||
return 'other'
|
||||
}
|
||||
|
||||
function inferSettingCategory(key: string): 'AI' | 'BRANDING' | 'EMAIL' | 'STORAGE' | 'SECURITY' | 'DEFAULTS' | 'WHATSAPP' | 'FEATURE_FLAGS' {
|
||||
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'
|
||||
if (key.startsWith('brand_') || key.startsWith('logo_') || key.startsWith('primary_') || key.startsWith('theme_')) return 'BRANDING'
|
||||
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'
|
||||
return 'DEFAULTS'
|
||||
}
|
||||
|
||||
export const settingsRouter = router({
|
||||
/**
|
||||
* Get public feature flags (no auth required)
|
||||
* These are non-sensitive settings that can be exposed to any user
|
||||
*/
|
||||
getFeatureFlags: protectedProcedure.query(async ({ ctx }) => {
|
||||
const [whatsappEnabled, juryCompareEnabled] = await Promise.all([
|
||||
const [whatsappEnabled, juryCompareEnabled, learningHubExternal, learningHubExternalUrl, supportEmail] = 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' },
|
||||
}),
|
||||
])
|
||||
|
||||
return {
|
||||
whatsappEnabled: whatsappEnabled?.value === 'true',
|
||||
juryCompareEnabled: juryCompareEnabled?.value === 'true',
|
||||
learningHubExternal: learningHubExternal?.value === 'true',
|
||||
learningHubExternalUrl: learningHubExternalUrl?.value || '',
|
||||
supportEmail: supportEmail?.value || '',
|
||||
}
|
||||
}),
|
||||
|
||||
@@ -120,12 +143,18 @@ export const settingsRouter = router({
|
||||
})
|
||||
)
|
||||
.mutation(async ({ ctx, input }) => {
|
||||
const setting = await ctx.prisma.systemSettings.update({
|
||||
const setting = await ctx.prisma.systemSettings.upsert({
|
||||
where: { key: input.key },
|
||||
data: {
|
||||
update: {
|
||||
value: input.value,
|
||||
updatedBy: ctx.user.id,
|
||||
},
|
||||
create: {
|
||||
key: input.key,
|
||||
value: input.value,
|
||||
category: inferSettingCategory(input.key),
|
||||
updatedBy: ctx.user.id,
|
||||
},
|
||||
})
|
||||
|
||||
// Clear storage provider cache when storage_provider setting changes
|
||||
@@ -161,23 +190,12 @@ export const settingsRouter = router({
|
||||
z.object({
|
||||
key: z.string(),
|
||||
value: z.string(),
|
||||
category: z.enum(['AI', 'BRANDING', 'EMAIL', 'STORAGE', 'SECURITY', 'DEFAULTS', 'WHATSAPP']).optional(),
|
||||
category: z.enum(['AI', 'BRANDING', 'EMAIL', 'STORAGE', 'SECURITY', 'DEFAULTS', 'WHATSAPP', 'FEATURE_FLAGS']).optional(),
|
||||
})
|
||||
),
|
||||
})
|
||||
)
|
||||
.mutation(async ({ ctx, input }) => {
|
||||
// Infer category from key prefix if not provided
|
||||
const inferCategory = (key: string): 'AI' | 'BRANDING' | 'EMAIL' | 'STORAGE' | 'SECURITY' | 'DEFAULTS' | 'WHATSAPP' => {
|
||||
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'
|
||||
if (key.startsWith('brand_') || key.startsWith('logo_') || key.startsWith('primary_') || key.startsWith('theme_')) return 'BRANDING'
|
||||
if (key.startsWith('whatsapp_')) return 'WHATSAPP'
|
||||
if (key.startsWith('security_') || key.startsWith('session_')) return 'SECURITY'
|
||||
return 'DEFAULTS'
|
||||
}
|
||||
|
||||
const results = await Promise.all(
|
||||
input.settings.map((s) =>
|
||||
ctx.prisma.systemSettings.upsert({
|
||||
@@ -189,7 +207,7 @@ export const settingsRouter = router({
|
||||
create: {
|
||||
key: s.key,
|
||||
value: s.value,
|
||||
category: s.category || inferCategory(s.key),
|
||||
category: s.category || inferSettingCategory(s.key),
|
||||
updatedBy: ctx.user.id,
|
||||
},
|
||||
})
|
||||
|
||||
Reference in New Issue
Block a user