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>
879 lines
29 KiB
TypeScript
879 lines
29 KiB
TypeScript
'use client'
|
|
|
|
import { trpc } from '@/lib/trpc/client'
|
|
import {
|
|
Card,
|
|
CardContent,
|
|
CardDescription,
|
|
CardHeader,
|
|
CardTitle,
|
|
} from '@/components/ui/card'
|
|
import { Tabs, TabsContent, TabsList, TabsTrigger } from '@/components/ui/tabs'
|
|
import { Skeleton } from '@/components/ui/skeleton'
|
|
import {
|
|
Cog,
|
|
Palette,
|
|
Mail,
|
|
HardDrive,
|
|
Shield,
|
|
Settings as SettingsIcon,
|
|
Bell,
|
|
Tags,
|
|
ExternalLink,
|
|
Newspaper,
|
|
BarChart3,
|
|
ShieldAlert,
|
|
Webhook,
|
|
MessageCircle,
|
|
} from 'lucide-react'
|
|
import Link from 'next/link'
|
|
import { AnimatedCard } from '@/components/shared/animated-container'
|
|
import { AISettingsForm } from './ai-settings-form'
|
|
import { AIUsageCard } from './ai-usage-card'
|
|
import { BrandingSettingsForm } from './branding-settings-form'
|
|
import { EmailSettingsForm } from './email-settings-form'
|
|
import { StorageSettingsForm } from './storage-settings-form'
|
|
import { SecuritySettingsForm } from './security-settings-form'
|
|
import { DefaultsSettingsForm } from './defaults-settings-form'
|
|
import { NotificationSettingsForm } from './notification-settings-form'
|
|
|
|
function SettingsSkeleton() {
|
|
return (
|
|
<div className="space-y-6">
|
|
<Skeleton className="h-10 w-full" />
|
|
<Card>
|
|
<CardHeader>
|
|
<Skeleton className="h-6 w-32" />
|
|
<Skeleton className="h-4 w-64" />
|
|
</CardHeader>
|
|
<CardContent>
|
|
<div className="space-y-4">
|
|
{[...Array(4)].map((_, i) => (
|
|
<Skeleton key={i} className="h-16 w-full" />
|
|
))}
|
|
</div>
|
|
</CardContent>
|
|
</Card>
|
|
</div>
|
|
)
|
|
}
|
|
|
|
interface SettingsContentProps {
|
|
initialSettings: Record<string, string>
|
|
isSuperAdmin?: boolean
|
|
}
|
|
|
|
export function SettingsContent({ initialSettings, isSuperAdmin = true }: SettingsContentProps) {
|
|
// We use the initial settings passed from the server
|
|
// Forms will refetch on mutation success
|
|
|
|
// Helper to get settings by prefix
|
|
const getSettingsByKeys = (keys: string[]) => {
|
|
const result: Record<string, string> = {}
|
|
keys.forEach((key) => {
|
|
if (initialSettings[key] !== undefined) {
|
|
result[key] = initialSettings[key]
|
|
}
|
|
})
|
|
return result
|
|
}
|
|
|
|
const aiSettings = getSettingsByKeys([
|
|
'ai_enabled',
|
|
'ai_provider',
|
|
'ai_model',
|
|
'ai_send_descriptions',
|
|
'openai_api_key',
|
|
'openai_base_url',
|
|
])
|
|
|
|
const brandingSettings = getSettingsByKeys([
|
|
'platform_name',
|
|
'primary_color',
|
|
'secondary_color',
|
|
'accent_color',
|
|
])
|
|
|
|
const emailSettings = getSettingsByKeys([
|
|
'smtp_host',
|
|
'smtp_port',
|
|
'smtp_user',
|
|
'smtp_password',
|
|
'email_from',
|
|
])
|
|
|
|
const storageSettings = getSettingsByKeys([
|
|
'storage_provider',
|
|
'local_storage_path',
|
|
'max_file_size_mb',
|
|
'avatar_max_size_mb',
|
|
'allowed_file_types',
|
|
'allowed_image_types',
|
|
])
|
|
|
|
const securitySettings = getSettingsByKeys([
|
|
'session_duration_hours',
|
|
'magic_link_expiry_minutes',
|
|
'invite_link_expiry_hours',
|
|
'rate_limit_requests_per_minute',
|
|
])
|
|
|
|
const defaultsSettings = getSettingsByKeys([
|
|
'default_timezone',
|
|
'default_page_size',
|
|
'autosave_interval_seconds',
|
|
'display_project_names_uppercase',
|
|
'learning_hub_external',
|
|
'learning_hub_external_url',
|
|
'support_email',
|
|
])
|
|
|
|
const digestSettings = getSettingsByKeys([
|
|
'digest_enabled',
|
|
'digest_default_frequency',
|
|
'digest_send_hour',
|
|
'digest_include_evaluations',
|
|
'digest_include_assignments',
|
|
'digest_include_deadlines',
|
|
'digest_include_announcements',
|
|
])
|
|
|
|
const analyticsSettings = getSettingsByKeys([
|
|
'analytics_observer_scores_tab',
|
|
'analytics_observer_progress_tab',
|
|
'analytics_observer_juror_tab',
|
|
'analytics_observer_comparison_tab',
|
|
'analytics_pdf_enabled',
|
|
'analytics_pdf_sections',
|
|
])
|
|
|
|
const auditSecuritySettings = getSettingsByKeys([
|
|
'audit_retention_days',
|
|
'anomaly_detection_enabled',
|
|
'anomaly_rapid_actions_threshold',
|
|
'anomaly_off_hours_start',
|
|
'anomaly_off_hours_end',
|
|
])
|
|
|
|
const whatsappSettings = getSettingsByKeys([
|
|
'whatsapp_enabled',
|
|
'whatsapp_provider',
|
|
])
|
|
|
|
return (
|
|
<>
|
|
<Tabs defaultValue="defaults" className="space-y-6">
|
|
{/* Mobile: horizontal scrollable tabs */}
|
|
<TabsList className="flex h-auto gap-1 overflow-x-auto whitespace-nowrap lg:hidden">
|
|
<TabsTrigger value="defaults" className="gap-2 shrink-0">
|
|
<SettingsIcon className="h-4 w-4" />
|
|
Defaults
|
|
</TabsTrigger>
|
|
<TabsTrigger value="branding" className="gap-2 shrink-0">
|
|
<Palette className="h-4 w-4" />
|
|
Branding
|
|
</TabsTrigger>
|
|
{isSuperAdmin && (
|
|
<TabsTrigger value="email" className="gap-2 shrink-0">
|
|
<Mail className="h-4 w-4" />
|
|
Email
|
|
</TabsTrigger>
|
|
)}
|
|
<TabsTrigger value="notifications" className="gap-2 shrink-0">
|
|
<Bell className="h-4 w-4" />
|
|
Notif.
|
|
</TabsTrigger>
|
|
<TabsTrigger value="digest" className="gap-2 shrink-0">
|
|
<Newspaper className="h-4 w-4" />
|
|
Digest
|
|
</TabsTrigger>
|
|
{isSuperAdmin && (
|
|
<TabsTrigger value="whatsapp" className="gap-2 shrink-0">
|
|
<MessageCircle className="h-4 w-4" />
|
|
WhatsApp
|
|
</TabsTrigger>
|
|
)}
|
|
{isSuperAdmin && (
|
|
<TabsTrigger value="security" className="gap-2 shrink-0">
|
|
<Shield className="h-4 w-4" />
|
|
Security
|
|
</TabsTrigger>
|
|
)}
|
|
<TabsTrigger value="audit" className="gap-2 shrink-0">
|
|
<ShieldAlert className="h-4 w-4" />
|
|
Audit
|
|
</TabsTrigger>
|
|
{isSuperAdmin && (
|
|
<TabsTrigger value="ai" className="gap-2 shrink-0">
|
|
<Cog className="h-4 w-4" />
|
|
AI
|
|
</TabsTrigger>
|
|
)}
|
|
<Link href="/admin/settings/tags" className="inline-flex items-center justify-center gap-2 shrink-0 rounded-md px-3 py-1 text-sm font-medium ring-offset-background transition-colors hover:bg-muted focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring">
|
|
<Tags className="h-4 w-4" />
|
|
Tags
|
|
</Link>
|
|
<TabsTrigger value="analytics" className="gap-2 shrink-0">
|
|
<BarChart3 className="h-4 w-4" />
|
|
Analytics
|
|
</TabsTrigger>
|
|
{isSuperAdmin && (
|
|
<TabsTrigger value="storage" className="gap-2 shrink-0">
|
|
<HardDrive className="h-4 w-4" />
|
|
Storage
|
|
</TabsTrigger>
|
|
)}
|
|
{isSuperAdmin && (
|
|
<Link href="/admin/settings/webhooks" className="inline-flex items-center justify-center gap-2 shrink-0 rounded-md px-3 py-1 text-sm font-medium ring-offset-background transition-colors hover:bg-muted focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring">
|
|
<Webhook className="h-4 w-4" />
|
|
Webhooks
|
|
</Link>
|
|
)}
|
|
</TabsList>
|
|
|
|
<div className="lg:flex lg:gap-8">
|
|
{/* Desktop: sidebar navigation */}
|
|
<div className="hidden lg:block lg:w-56 lg:shrink-0">
|
|
<nav className="space-y-6">
|
|
<div>
|
|
<p className="mb-2 px-3 text-xs font-semibold uppercase tracking-wider text-muted-foreground">General</p>
|
|
<TabsList className="flex flex-col items-stretch h-auto w-full bg-transparent p-0 gap-0.5">
|
|
<TabsTrigger value="defaults" className="justify-start gap-2 w-full px-3 py-2 h-auto data-[state=active]:bg-muted">
|
|
<SettingsIcon className="h-4 w-4" />
|
|
Defaults
|
|
</TabsTrigger>
|
|
<TabsTrigger value="branding" className="justify-start gap-2 w-full px-3 py-2 h-auto data-[state=active]:bg-muted">
|
|
<Palette className="h-4 w-4" />
|
|
Branding
|
|
</TabsTrigger>
|
|
</TabsList>
|
|
</div>
|
|
<div>
|
|
<p className="mb-2 px-3 text-xs font-semibold uppercase tracking-wider text-muted-foreground">Communication</p>
|
|
<TabsList className="flex flex-col items-stretch h-auto w-full bg-transparent p-0 gap-0.5">
|
|
{isSuperAdmin && (
|
|
<TabsTrigger value="email" className="justify-start gap-2 w-full px-3 py-2 h-auto data-[state=active]:bg-muted">
|
|
<Mail className="h-4 w-4" />
|
|
Email
|
|
</TabsTrigger>
|
|
)}
|
|
<TabsTrigger value="notifications" className="justify-start gap-2 w-full px-3 py-2 h-auto data-[state=active]:bg-muted">
|
|
<Bell className="h-4 w-4" />
|
|
Notifications
|
|
</TabsTrigger>
|
|
<TabsTrigger value="digest" className="justify-start gap-2 w-full px-3 py-2 h-auto data-[state=active]:bg-muted">
|
|
<Newspaper className="h-4 w-4" />
|
|
Digest
|
|
</TabsTrigger>
|
|
{isSuperAdmin && (
|
|
<TabsTrigger value="whatsapp" className="justify-start gap-2 w-full px-3 py-2 h-auto data-[state=active]:bg-muted">
|
|
<MessageCircle className="h-4 w-4" />
|
|
WhatsApp
|
|
</TabsTrigger>
|
|
)}
|
|
</TabsList>
|
|
</div>
|
|
<div>
|
|
<p className="mb-2 px-3 text-xs font-semibold uppercase tracking-wider text-muted-foreground">Security</p>
|
|
<TabsList className="flex flex-col items-stretch h-auto w-full bg-transparent p-0 gap-0.5">
|
|
{isSuperAdmin && (
|
|
<TabsTrigger value="security" className="justify-start gap-2 w-full px-3 py-2 h-auto data-[state=active]:bg-muted">
|
|
<Shield className="h-4 w-4" />
|
|
Security
|
|
</TabsTrigger>
|
|
)}
|
|
<TabsTrigger value="audit" className="justify-start gap-2 w-full px-3 py-2 h-auto data-[state=active]:bg-muted">
|
|
<ShieldAlert className="h-4 w-4" />
|
|
Audit
|
|
</TabsTrigger>
|
|
</TabsList>
|
|
</div>
|
|
<div>
|
|
<p className="mb-2 px-3 text-xs font-semibold uppercase tracking-wider text-muted-foreground">Features</p>
|
|
<TabsList className="flex flex-col items-stretch h-auto w-full bg-transparent p-0 gap-0.5">
|
|
{isSuperAdmin && (
|
|
<TabsTrigger value="ai" className="justify-start gap-2 w-full px-3 py-2 h-auto data-[state=active]:bg-muted">
|
|
<Cog className="h-4 w-4" />
|
|
AI
|
|
</TabsTrigger>
|
|
)}
|
|
<Link href="/admin/settings/tags" className="inline-flex items-center justify-start gap-2 w-full px-3 py-2 rounded-md text-sm font-medium text-muted-foreground hover:bg-muted hover:text-foreground transition-colors">
|
|
<Tags className="h-4 w-4" />
|
|
Tags
|
|
<ExternalLink className="ml-auto h-3 w-3 opacity-50" />
|
|
</Link>
|
|
<TabsTrigger value="analytics" className="justify-start gap-2 w-full px-3 py-2 h-auto data-[state=active]:bg-muted">
|
|
<BarChart3 className="h-4 w-4" />
|
|
Analytics
|
|
</TabsTrigger>
|
|
</TabsList>
|
|
</div>
|
|
{isSuperAdmin && (
|
|
<div>
|
|
<p className="mb-2 px-3 text-xs font-semibold uppercase tracking-wider text-muted-foreground">Infrastructure</p>
|
|
<TabsList className="flex flex-col items-stretch h-auto w-full bg-transparent p-0 gap-0.5">
|
|
<TabsTrigger value="storage" className="justify-start gap-2 w-full px-3 py-2 h-auto data-[state=active]:bg-muted">
|
|
<HardDrive className="h-4 w-4" />
|
|
Storage
|
|
</TabsTrigger>
|
|
</TabsList>
|
|
<Link href="/admin/settings/webhooks" className="inline-flex items-center justify-start gap-2 w-full px-3 py-2 rounded-md text-sm font-medium text-muted-foreground hover:bg-muted hover:text-foreground transition-colors">
|
|
<Webhook className="h-4 w-4" />
|
|
Webhooks
|
|
<ExternalLink className="ml-auto h-3 w-3 opacity-50" />
|
|
</Link>
|
|
</div>
|
|
)}
|
|
</nav>
|
|
</div>
|
|
|
|
{/* Content area */}
|
|
<div className="flex-1 min-w-0">
|
|
|
|
{isSuperAdmin && (
|
|
<TabsContent value="ai" className="space-y-6">
|
|
<AnimatedCard>
|
|
<Card>
|
|
<CardHeader>
|
|
<CardTitle>AI Configuration</CardTitle>
|
|
<CardDescription>
|
|
Configure AI-powered features like smart jury assignment
|
|
</CardDescription>
|
|
</CardHeader>
|
|
<CardContent>
|
|
<AISettingsForm settings={aiSettings} />
|
|
</CardContent>
|
|
</Card>
|
|
</AnimatedCard>
|
|
<AIUsageCard />
|
|
</TabsContent>
|
|
)}
|
|
|
|
<TabsContent value="branding">
|
|
<AnimatedCard>
|
|
<Card>
|
|
<CardHeader>
|
|
<CardTitle>Platform Branding</CardTitle>
|
|
<CardDescription>
|
|
Customize the look and feel of your platform
|
|
</CardDescription>
|
|
</CardHeader>
|
|
<CardContent>
|
|
<BrandingSettingsForm settings={brandingSettings} />
|
|
</CardContent>
|
|
</Card>
|
|
</AnimatedCard>
|
|
</TabsContent>
|
|
|
|
{isSuperAdmin && (
|
|
<TabsContent value="email">
|
|
<AnimatedCard>
|
|
<Card>
|
|
<CardHeader>
|
|
<CardTitle>Email Configuration</CardTitle>
|
|
<CardDescription>
|
|
Configure email settings for notifications and magic links
|
|
</CardDescription>
|
|
</CardHeader>
|
|
<CardContent>
|
|
<EmailSettingsForm settings={emailSettings} />
|
|
</CardContent>
|
|
</Card>
|
|
</AnimatedCard>
|
|
</TabsContent>
|
|
)}
|
|
|
|
<TabsContent value="notifications">
|
|
<AnimatedCard>
|
|
<Card>
|
|
<CardHeader>
|
|
<CardTitle>Notification Email Settings</CardTitle>
|
|
<CardDescription>
|
|
Configure which notification types should also send email notifications
|
|
</CardDescription>
|
|
</CardHeader>
|
|
<CardContent>
|
|
<NotificationSettingsForm />
|
|
</CardContent>
|
|
</Card>
|
|
</AnimatedCard>
|
|
</TabsContent>
|
|
|
|
{isSuperAdmin && (
|
|
<TabsContent value="storage">
|
|
<AnimatedCard>
|
|
<Card>
|
|
<CardHeader>
|
|
<CardTitle>File Storage</CardTitle>
|
|
<CardDescription>
|
|
Configure file upload limits and allowed types
|
|
</CardDescription>
|
|
</CardHeader>
|
|
<CardContent>
|
|
<StorageSettingsForm settings={storageSettings} />
|
|
</CardContent>
|
|
</Card>
|
|
</AnimatedCard>
|
|
</TabsContent>
|
|
)}
|
|
|
|
{isSuperAdmin && (
|
|
<TabsContent value="security">
|
|
<AnimatedCard>
|
|
<Card>
|
|
<CardHeader>
|
|
<CardTitle>Security Settings</CardTitle>
|
|
<CardDescription>
|
|
Configure security and access control settings
|
|
</CardDescription>
|
|
</CardHeader>
|
|
<CardContent>
|
|
<SecuritySettingsForm settings={securitySettings} />
|
|
</CardContent>
|
|
</Card>
|
|
</AnimatedCard>
|
|
</TabsContent>
|
|
)}
|
|
|
|
<TabsContent value="defaults" className="space-y-6">
|
|
<AnimatedCard>
|
|
<Card>
|
|
<CardHeader>
|
|
<CardTitle>Default Settings</CardTitle>
|
|
<CardDescription>
|
|
Configure default values for the platform
|
|
</CardDescription>
|
|
</CardHeader>
|
|
<CardContent>
|
|
<DefaultsSettingsForm settings={defaultsSettings} />
|
|
</CardContent>
|
|
</Card>
|
|
</AnimatedCard>
|
|
|
|
<AnimatedCard>
|
|
<Card>
|
|
<CardHeader>
|
|
<CardTitle>Platform Features</CardTitle>
|
|
<CardDescription>
|
|
Configure Learning Hub, support contact, and other platform features
|
|
</CardDescription>
|
|
</CardHeader>
|
|
<CardContent>
|
|
<PlatformFeaturesSection settings={defaultsSettings} />
|
|
</CardContent>
|
|
</Card>
|
|
</AnimatedCard>
|
|
</TabsContent>
|
|
|
|
<TabsContent value="digest" className="space-y-6">
|
|
<AnimatedCard>
|
|
<Card>
|
|
<CardHeader>
|
|
<CardTitle>Digest Configuration</CardTitle>
|
|
<CardDescription>
|
|
Configure automated digest emails sent to users
|
|
</CardDescription>
|
|
</CardHeader>
|
|
<CardContent>
|
|
<DigestSettingsSection settings={digestSettings} />
|
|
</CardContent>
|
|
</Card>
|
|
</AnimatedCard>
|
|
</TabsContent>
|
|
|
|
<TabsContent value="analytics" className="space-y-6">
|
|
<AnimatedCard>
|
|
<Card>
|
|
<CardHeader>
|
|
<CardTitle>Analytics & Reports</CardTitle>
|
|
<CardDescription>
|
|
Configure observer dashboard visibility and PDF report settings
|
|
</CardDescription>
|
|
</CardHeader>
|
|
<CardContent>
|
|
<AnalyticsSettingsSection settings={analyticsSettings} />
|
|
</CardContent>
|
|
</Card>
|
|
</AnimatedCard>
|
|
</TabsContent>
|
|
|
|
<TabsContent value="audit" className="space-y-6">
|
|
<AnimatedCard>
|
|
<Card>
|
|
<CardHeader>
|
|
<CardTitle>Audit & Security</CardTitle>
|
|
<CardDescription>
|
|
Configure audit log retention and anomaly detection
|
|
</CardDescription>
|
|
</CardHeader>
|
|
<CardContent>
|
|
<AuditSettingsSection settings={auditSecuritySettings} />
|
|
</CardContent>
|
|
</Card>
|
|
</AnimatedCard>
|
|
</TabsContent>
|
|
|
|
{isSuperAdmin && (
|
|
<TabsContent value="whatsapp" className="space-y-6">
|
|
<AnimatedCard>
|
|
<Card>
|
|
<CardHeader>
|
|
<CardTitle>WhatsApp Notifications</CardTitle>
|
|
<CardDescription>
|
|
Configure WhatsApp messaging for notifications
|
|
</CardDescription>
|
|
</CardHeader>
|
|
<CardContent>
|
|
<WhatsAppSettingsSection settings={whatsappSettings} />
|
|
</CardContent>
|
|
</Card>
|
|
</AnimatedCard>
|
|
</TabsContent>
|
|
)}
|
|
</div>{/* end content area */}
|
|
</div>{/* end lg:flex */}
|
|
</Tabs>
|
|
|
|
</>
|
|
)
|
|
}
|
|
|
|
export { SettingsSkeleton }
|
|
|
|
// Inline settings sections for new tabs
|
|
|
|
import { useState } from 'react'
|
|
import { Switch } from '@/components/ui/switch'
|
|
import { Input } from '@/components/ui/input'
|
|
import { Label } from '@/components/ui/label'
|
|
import { Checkbox } from '@/components/ui/checkbox'
|
|
import {
|
|
Select,
|
|
SelectContent,
|
|
SelectItem,
|
|
SelectTrigger,
|
|
SelectValue,
|
|
} from '@/components/ui/select'
|
|
import { Loader2 } from 'lucide-react'
|
|
import { toast } from 'sonner'
|
|
|
|
function useSettingsMutation() {
|
|
const utils = trpc.useUtils()
|
|
return trpc.settings.update.useMutation({
|
|
onSuccess: () => {
|
|
utils.settings.invalidate()
|
|
toast.success('Setting updated')
|
|
},
|
|
onError: (e) => toast.error(e.message),
|
|
})
|
|
}
|
|
|
|
function SettingToggle({
|
|
label,
|
|
description,
|
|
settingKey,
|
|
value,
|
|
}: {
|
|
label: string
|
|
description?: string
|
|
settingKey: string
|
|
value: string
|
|
}) {
|
|
const mutation = useSettingsMutation()
|
|
const isChecked = value === 'true'
|
|
|
|
return (
|
|
<div className="flex items-center justify-between rounded-lg border p-3">
|
|
<div className="space-y-0.5">
|
|
<Label className="text-sm font-medium">{label}</Label>
|
|
{description && (
|
|
<p className="text-xs text-muted-foreground">{description}</p>
|
|
)}
|
|
</div>
|
|
<Switch
|
|
checked={isChecked}
|
|
disabled={mutation.isPending}
|
|
onCheckedChange={(checked) =>
|
|
mutation.mutate({ key: settingKey, value: String(checked) })
|
|
}
|
|
/>
|
|
</div>
|
|
)
|
|
}
|
|
|
|
function SettingInput({
|
|
label,
|
|
description,
|
|
settingKey,
|
|
value,
|
|
type = 'text',
|
|
}: {
|
|
label: string
|
|
description?: string
|
|
settingKey: string
|
|
value: string
|
|
type?: string
|
|
}) {
|
|
const [localValue, setLocalValue] = useState(value)
|
|
const mutation = useSettingsMutation()
|
|
|
|
const save = () => {
|
|
if (localValue !== value) {
|
|
mutation.mutate({ key: settingKey, value: localValue })
|
|
}
|
|
}
|
|
|
|
return (
|
|
<div className="space-y-2">
|
|
<Label className="text-sm font-medium">{label}</Label>
|
|
{description && (
|
|
<p className="text-xs text-muted-foreground">{description}</p>
|
|
)}
|
|
<div className="flex gap-2">
|
|
<Input
|
|
type={type}
|
|
value={localValue}
|
|
onChange={(e) => setLocalValue(e.target.value)}
|
|
onBlur={save}
|
|
className="max-w-xs"
|
|
/>
|
|
{mutation.isPending && <Loader2 className="h-4 w-4 animate-spin self-center" />}
|
|
</div>
|
|
</div>
|
|
)
|
|
}
|
|
|
|
function SettingSelect({
|
|
label,
|
|
description,
|
|
settingKey,
|
|
value,
|
|
options,
|
|
}: {
|
|
label: string
|
|
description?: string
|
|
settingKey: string
|
|
value: string
|
|
options: Array<{ value: string; label: string }>
|
|
}) {
|
|
const mutation = useSettingsMutation()
|
|
|
|
return (
|
|
<div className="space-y-2">
|
|
<Label className="text-sm font-medium">{label}</Label>
|
|
{description && (
|
|
<p className="text-xs text-muted-foreground">{description}</p>
|
|
)}
|
|
<Select
|
|
value={value || options[0]?.value}
|
|
onValueChange={(v) => mutation.mutate({ key: settingKey, value: v })}
|
|
disabled={mutation.isPending}
|
|
>
|
|
<SelectTrigger className="max-w-xs">
|
|
<SelectValue />
|
|
</SelectTrigger>
|
|
<SelectContent>
|
|
{options.map((opt) => (
|
|
<SelectItem key={opt.value} value={opt.value}>
|
|
{opt.label}
|
|
</SelectItem>
|
|
))}
|
|
</SelectContent>
|
|
</Select>
|
|
</div>
|
|
)
|
|
}
|
|
|
|
function DigestSettingsSection({ settings }: { settings: Record<string, string> }) {
|
|
return (
|
|
<div className="space-y-4">
|
|
<SettingToggle
|
|
label="Enable Digest Emails"
|
|
description="Send periodic digest emails summarizing platform activity"
|
|
settingKey="digest_enabled"
|
|
value={settings.digest_enabled || 'false'}
|
|
/>
|
|
<SettingSelect
|
|
label="Default Frequency"
|
|
description="How often digests are sent to users by default"
|
|
settingKey="digest_default_frequency"
|
|
value={settings.digest_default_frequency || 'weekly'}
|
|
options={[
|
|
{ value: 'daily', label: 'Daily' },
|
|
{ value: 'weekly', label: 'Weekly' },
|
|
{ value: 'biweekly', label: 'Bi-weekly' },
|
|
{ value: 'monthly', label: 'Monthly' },
|
|
]}
|
|
/>
|
|
<SettingInput
|
|
label="Send Hour (UTC)"
|
|
description="Hour of day when digest emails are sent (0-23)"
|
|
settingKey="digest_send_hour"
|
|
value={settings.digest_send_hour || '8'}
|
|
type="number"
|
|
/>
|
|
<div className="border-t pt-4 space-y-3">
|
|
<Label className="text-sm font-medium">Digest Sections</Label>
|
|
<SettingToggle
|
|
label="Include Evaluations"
|
|
settingKey="digest_include_evaluations"
|
|
value={settings.digest_include_evaluations || 'true'}
|
|
/>
|
|
<SettingToggle
|
|
label="Include Assignments"
|
|
settingKey="digest_include_assignments"
|
|
value={settings.digest_include_assignments || 'true'}
|
|
/>
|
|
<SettingToggle
|
|
label="Include Deadlines"
|
|
settingKey="digest_include_deadlines"
|
|
value={settings.digest_include_deadlines || 'true'}
|
|
/>
|
|
<SettingToggle
|
|
label="Include Announcements"
|
|
settingKey="digest_include_announcements"
|
|
value={settings.digest_include_announcements || 'true'}
|
|
/>
|
|
</div>
|
|
</div>
|
|
)
|
|
}
|
|
|
|
function AnalyticsSettingsSection({ settings }: { settings: Record<string, string> }) {
|
|
return (
|
|
<div className="space-y-4">
|
|
<Label className="text-sm font-medium">Observer Tab Visibility</Label>
|
|
<p className="text-xs text-muted-foreground">
|
|
Choose which analytics tabs are visible to observers
|
|
</p>
|
|
<SettingToggle
|
|
label="Scores Tab"
|
|
settingKey="analytics_observer_scores_tab"
|
|
value={settings.analytics_observer_scores_tab || 'true'}
|
|
/>
|
|
<SettingToggle
|
|
label="Progress Tab"
|
|
settingKey="analytics_observer_progress_tab"
|
|
value={settings.analytics_observer_progress_tab || 'true'}
|
|
/>
|
|
<SettingToggle
|
|
label="Juror Stats Tab"
|
|
settingKey="analytics_observer_juror_tab"
|
|
value={settings.analytics_observer_juror_tab || 'true'}
|
|
/>
|
|
<SettingToggle
|
|
label="Comparison Tab"
|
|
settingKey="analytics_observer_comparison_tab"
|
|
value={settings.analytics_observer_comparison_tab || 'true'}
|
|
/>
|
|
<div className="border-t pt-4 space-y-3">
|
|
<Label className="text-sm font-medium">PDF Reports</Label>
|
|
<SettingToggle
|
|
label="Enable PDF Report Generation"
|
|
description="Allow admins and observers to generate PDF reports"
|
|
settingKey="analytics_pdf_enabled"
|
|
value={settings.analytics_pdf_enabled || 'true'}
|
|
/>
|
|
</div>
|
|
</div>
|
|
)
|
|
}
|
|
|
|
function AuditSettingsSection({ settings }: { settings: Record<string, string> }) {
|
|
return (
|
|
<div className="space-y-4">
|
|
<SettingInput
|
|
label="Retention Period (days)"
|
|
description="How long audit log entries are kept before automatic cleanup"
|
|
settingKey="audit_retention_days"
|
|
value={settings.audit_retention_days || '365'}
|
|
type="number"
|
|
/>
|
|
<div className="border-t pt-4 space-y-3">
|
|
<SettingToggle
|
|
label="Enable Anomaly Detection"
|
|
description="Detect suspicious patterns like rapid actions or off-hours access"
|
|
settingKey="anomaly_detection_enabled"
|
|
value={settings.anomaly_detection_enabled || 'false'}
|
|
/>
|
|
<SettingInput
|
|
label="Rapid Actions Threshold"
|
|
description="Maximum actions per minute before flagging as anomalous"
|
|
settingKey="anomaly_rapid_actions_threshold"
|
|
value={settings.anomaly_rapid_actions_threshold || '30'}
|
|
type="number"
|
|
/>
|
|
<SettingInput
|
|
label="Off-Hours Start (UTC)"
|
|
description="Start hour for off-hours monitoring (0-23)"
|
|
settingKey="anomaly_off_hours_start"
|
|
value={settings.anomaly_off_hours_start || '22'}
|
|
type="number"
|
|
/>
|
|
<SettingInput
|
|
label="Off-Hours End (UTC)"
|
|
description="End hour for off-hours monitoring (0-23)"
|
|
settingKey="anomaly_off_hours_end"
|
|
value={settings.anomaly_off_hours_end || '6'}
|
|
type="number"
|
|
/>
|
|
</div>
|
|
</div>
|
|
)
|
|
}
|
|
|
|
function PlatformFeaturesSection({ settings }: { settings: Record<string, string> }) {
|
|
return (
|
|
<div className="space-y-6">
|
|
<div className="space-y-4">
|
|
<Label className="text-sm font-medium">Learning Hub</Label>
|
|
<SettingToggle
|
|
label="Use External Learning Hub"
|
|
description="When enabled, jury and mentor navigation links will open the external URL instead of the built-in Learning Hub"
|
|
settingKey="learning_hub_external"
|
|
value={settings.learning_hub_external || 'false'}
|
|
/>
|
|
<SettingInput
|
|
label="External URL"
|
|
description="The URL to redirect jury and mentor users to (e.g. Google Drive, Notion, etc.)"
|
|
settingKey="learning_hub_external_url"
|
|
value={settings.learning_hub_external_url || ''}
|
|
/>
|
|
</div>
|
|
<div className="border-t pt-4 space-y-4">
|
|
<Label className="text-sm font-medium">Support</Label>
|
|
<SettingInput
|
|
label="Support Email"
|
|
description="Shown as a help button on the applicant page header. Leave empty to hide."
|
|
settingKey="support_email"
|
|
value={settings.support_email || ''}
|
|
/>
|
|
</div>
|
|
</div>
|
|
)
|
|
}
|
|
|
|
function WhatsAppSettingsSection({ settings }: { settings: Record<string, string> }) {
|
|
return (
|
|
<div className="space-y-4">
|
|
<SettingToggle
|
|
label="Enable WhatsApp Notifications"
|
|
description="Send notifications via WhatsApp in addition to email"
|
|
settingKey="whatsapp_enabled"
|
|
value={settings.whatsapp_enabled || 'false'}
|
|
/>
|
|
<SettingSelect
|
|
label="WhatsApp Provider"
|
|
description="Select the API provider for sending WhatsApp messages"
|
|
settingKey="whatsapp_provider"
|
|
value={settings.whatsapp_provider || 'META'}
|
|
options={[
|
|
{ value: 'META', label: 'Meta (WhatsApp Business API)' },
|
|
{ value: 'TWILIO', label: 'Twilio' },
|
|
]}
|
|
/>
|
|
</div>
|
|
)
|
|
}
|
|
|