Platform-wide UX fixes: assignment dialog, invalidation, settings, dashboard
All checks were successful
Build and Push Docker Image / build (push) Successful in 8m4s

1. Assignment dialog overhaul: replace raw UUID inputs with searchable
   juror Combobox (shows name, email, capacity) and multi-select project
   checklist with bulk assignment support

2. Query invalidation sweep: fix missing invalidations in
   assignment-preview-sheet (roundAssignment.execute) and
   filtering-dashboard (filtering.finalizeResults) so data refreshes
   without page reload

3. Rename Submissions tab to Document Windows with descriptive
   header explaining upload window configuration

4. Connect 6 disconnected settings: storage_provider, local_storage_path,
   avatar_max_size_mb, allowed_image_types, whatsapp_enabled,
   whatsapp_provider - all now accessible in Settings UI

5. Admin dashboard redesign: branded Editorial Command Center with
   Dark Blue gradient header, colored border-l-4 stat cards, staggered
   animations, 2-column layout, action-required panel, activity timeline

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
Matt
2026-02-16 16:05:25 +01:00
parent b2279067e2
commit 5965f7889d
7 changed files with 1086 additions and 614 deletions

View File

@@ -25,6 +25,7 @@ import {
ShieldAlert,
Globe,
Webhook,
MessageCircle,
} from 'lucide-react'
import Link from 'next/link'
import { AnimatedCard } from '@/components/shared/animated-container'
@@ -103,8 +104,12 @@ export function SettingsContent({ initialSettings, isSuperAdmin = true }: Settin
])
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([
@@ -147,6 +152,11 @@ export function SettingsContent({ initialSettings, isSuperAdmin = true }: Settin
'anomaly_off_hours_end',
])
const whatsappSettings = getSettingsByKeys([
'whatsapp_enabled',
'whatsapp_provider',
])
const localizationSettings = getSettingsByKeys([
'localization_enabled_locales',
'localization_default_locale',
@@ -183,6 +193,12 @@ export function SettingsContent({ initialSettings, isSuperAdmin = true }: Settin
<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" />
@@ -259,6 +275,12 @@ export function SettingsContent({ initialSettings, isSuperAdmin = true }: Settin
<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>
@@ -502,6 +524,24 @@ export function SettingsContent({ initialSettings, isSuperAdmin = true }: Settin
</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>
@@ -794,6 +834,29 @@ function AuditSettingsSection({ settings }: { settings: Record<string, string> }
)
}
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>
)
}
function LocalizationSettingsSection({ settings }: { settings: Record<string, string> }) {
const mutation = useSettingsMutation()
const enabledLocales = (settings.localization_enabled_locales || 'en').split(',')

View File

@@ -22,6 +22,14 @@ import {
} from '@/components/ui/form'
// Note: Storage provider cache is cleared server-side when settings are updated
const COMMON_IMAGE_TYPES = [
{ value: 'image/png', label: 'PNG (.png)' },
{ value: 'image/jpeg', label: 'JPEG (.jpg, .jpeg)' },
{ value: 'image/webp', label: 'WebP (.webp)' },
{ value: 'image/gif', label: 'GIF (.gif)' },
{ value: 'image/svg+xml', label: 'SVG (.svg)' },
]
const COMMON_FILE_TYPES = [
{ value: 'application/pdf', label: 'PDF Documents (.pdf)' },
{ value: 'video/mp4', label: 'MP4 Video (.mp4)' },
@@ -41,6 +49,7 @@ const formSchema = z.object({
max_file_size_mb: z.string().regex(/^\d+$/, 'Must be a number'),
avatar_max_size_mb: z.string().regex(/^\d+$/, 'Must be a number'),
allowed_file_types: z.array(z.string()).min(1, 'Select at least one file type'),
allowed_image_types: z.array(z.string()).min(1, 'Select at least one image type'),
})
type FormValues = z.infer<typeof formSchema>
@@ -52,6 +61,7 @@ interface StorageSettingsFormProps {
max_file_size_mb?: string
avatar_max_size_mb?: string
allowed_file_types?: string
allowed_image_types?: string
}
}
@@ -68,6 +78,16 @@ export function StorageSettingsForm({ settings }: StorageSettingsFormProps) {
allowedTypes = ['application/pdf', 'video/mp4', 'video/quicktime', 'image/png', 'image/jpeg']
}
// Parse allowed image types from JSON string
let allowedImageTypes: string[] = []
try {
allowedImageTypes = settings.allowed_image_types
? JSON.parse(settings.allowed_image_types)
: ['image/png', 'image/jpeg', 'image/webp']
} catch {
allowedImageTypes = ['image/png', 'image/jpeg', 'image/webp']
}
const form = useForm<FormValues>({
resolver: zodResolver(formSchema),
defaultValues: {
@@ -76,6 +96,7 @@ export function StorageSettingsForm({ settings }: StorageSettingsFormProps) {
max_file_size_mb: settings.max_file_size_mb || '500',
avatar_max_size_mb: settings.avatar_max_size_mb || '5',
allowed_file_types: allowedTypes,
allowed_image_types: allowedImageTypes,
},
})
@@ -99,6 +120,7 @@ export function StorageSettingsForm({ settings }: StorageSettingsFormProps) {
{ key: 'max_file_size_mb', value: data.max_file_size_mb },
{ key: 'avatar_max_size_mb', value: data.avatar_max_size_mb },
{ key: 'allowed_file_types', value: JSON.stringify(data.allowed_file_types) },
{ key: 'allowed_image_types', value: JSON.stringify(data.allowed_image_types) },
],
})
}
@@ -255,6 +277,57 @@ export function StorageSettingsForm({ settings }: StorageSettingsFormProps) {
)}
/>
<FormField
control={form.control}
name="allowed_image_types"
render={() => (
<FormItem>
<div className="mb-4">
<FormLabel>Allowed Image Types (Avatars/Logos)</FormLabel>
<FormDescription>
Select which image formats can be used for profile pictures and project logos
</FormDescription>
</div>
<div className="grid gap-3 md:grid-cols-2">
{COMMON_IMAGE_TYPES.map((type) => (
<FormField
key={type.value}
control={form.control}
name="allowed_image_types"
render={({ field }) => {
return (
<FormItem
key={type.value}
className="flex items-start space-x-3 space-y-0"
>
<FormControl>
<Checkbox
checked={field.value?.includes(type.value)}
onCheckedChange={(checked) => {
return checked
? field.onChange([...field.value, type.value])
: field.onChange(
field.value?.filter(
(value) => value !== type.value
)
)
}}
/>
</FormControl>
<FormLabel className="cursor-pointer text-sm font-normal">
{type.label}
</FormLabel>
</FormItem>
)
}}
/>
))}
</div>
<FormMessage />
</FormItem>
)}
/>
{storageProvider === 's3' && (
<div className="rounded-lg border border-muted bg-muted/50 p-4">
<p className="text-sm text-muted-foreground">