Multi-role members, round detail UI overhaul, dashboard jury progress, and submit bug fix
Some checks failed
Build and Push Docker Image / build (push) Has been cancelled
Some checks failed
Build and Push Docker Image / build (push) Has been cancelled
- Add roles UserRole[] to User model with migration + backfill from existing role column - Update auth JWT/session to propagate roles array with [role] fallback for stale tokens - Update tRPC hasRole() middleware and add userHasRole() helper for inline role checks - Update ~15 router inline checks and ~13 DB queries to use roles array - Add updateRoles admin mutation with SUPER_ADMIN guard and priority-based primary role - Add role switcher UI in admin sidebar and role-nav for multi-role users - Remove redundant stats cards from round detail, add window dates to header banner - Merge Members section into JuryProgressTable with inline cap editor and remove buttons - Reorder round detail assignments tab: Progress > Score Dist > Assignments > Coverage > Jury Group - Make score distribution fill full vertical height, reassignment history always open - Add per-juror progress bars to admin dashboard ActiveRoundPanel for EVALUATION rounds - Fix evaluation submit bug: use isSubmitting state instead of startMutation.isPending Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -35,7 +35,10 @@ import {
|
||||
LayoutTemplate,
|
||||
Layers,
|
||||
Scale,
|
||||
Eye,
|
||||
ArrowRightLeft,
|
||||
} from 'lucide-react'
|
||||
import type { UserRole } from '@prisma/client'
|
||||
import { getInitials } from '@/lib/utils'
|
||||
import { Logo } from '@/components/shared/logo'
|
||||
import { EditionSelector } from '@/components/shared/edition-selector'
|
||||
@@ -147,12 +150,21 @@ const roleLabels: Record<string, string> = {
|
||||
PROGRAM_ADMIN: 'Program Admin',
|
||||
JURY_MEMBER: 'Jury Member',
|
||||
OBSERVER: 'Observer',
|
||||
MENTOR: 'Mentor',
|
||||
AWARD_MASTER: 'Award Master',
|
||||
}
|
||||
|
||||
// Role switcher config — maps roles to their dashboard views
|
||||
const ROLE_SWITCH_OPTIONS: Record<string, { label: string; path: string; icon: typeof LayoutDashboard }> = {
|
||||
JURY_MEMBER: { label: 'Jury View', path: '/jury', icon: Scale },
|
||||
MENTOR: { label: 'Mentor View', path: '/mentor', icon: Handshake },
|
||||
OBSERVER: { label: 'Observer View', path: '/observer', icon: Eye },
|
||||
}
|
||||
|
||||
export function AdminSidebar({ user }: AdminSidebarProps) {
|
||||
const pathname = usePathname()
|
||||
const [isMobileMenuOpen, setIsMobileMenuOpen] = useState(false)
|
||||
const { status: sessionStatus } = useSession()
|
||||
const { data: session, status: sessionStatus } = useSession()
|
||||
const isAuthenticated = sessionStatus === 'authenticated'
|
||||
const { data: avatarUrl } = trpc.avatar.getUrl.useQuery(undefined, {
|
||||
enabled: isAuthenticated,
|
||||
@@ -162,6 +174,12 @@ export function AdminSidebar({ user }: AdminSidebarProps) {
|
||||
const isSuperAdmin = user.role === 'SUPER_ADMIN'
|
||||
const roleLabel = roleLabels[user.role || ''] || 'User'
|
||||
|
||||
// Roles the user can switch to (non-admin roles they hold)
|
||||
const userRoles = (session?.user?.roles as UserRole[] | undefined) ?? []
|
||||
const switchableRoles = Object.entries(ROLE_SWITCH_OPTIONS).filter(
|
||||
([role]) => userRoles.includes(role as UserRole)
|
||||
)
|
||||
|
||||
// Build dynamic admin nav with current edition's apply page
|
||||
const dynamicAdminNav = adminNavigation.map((item) => {
|
||||
if (item.name === 'Apply Page' && currentEdition?.id) {
|
||||
@@ -344,6 +362,29 @@ export function AdminSidebar({ user }: AdminSidebarProps) {
|
||||
</Link>
|
||||
</DropdownMenuItem>
|
||||
|
||||
{switchableRoles.length > 0 && (
|
||||
<>
|
||||
<DropdownMenuSeparator className="my-1" />
|
||||
<div className="px-2 py-1.5">
|
||||
<p className="flex items-center gap-1.5 text-[11px] font-medium uppercase tracking-wider text-muted-foreground/60">
|
||||
<ArrowRightLeft className="h-3 w-3" />
|
||||
Switch View
|
||||
</p>
|
||||
</div>
|
||||
{switchableRoles.map(([, opt]) => (
|
||||
<DropdownMenuItem key={opt.path} asChild>
|
||||
<Link
|
||||
href={opt.path as Route}
|
||||
className="flex cursor-pointer items-center gap-2.5 rounded-md px-2 py-2"
|
||||
>
|
||||
<opt.icon className="h-4 w-4 text-muted-foreground" />
|
||||
<span>{opt.label}</span>
|
||||
</Link>
|
||||
</DropdownMenuItem>
|
||||
))}
|
||||
</>
|
||||
)}
|
||||
|
||||
<DropdownMenuSeparator className="my-1" />
|
||||
|
||||
<DropdownMenuItem
|
||||
|
||||
Reference in New Issue
Block a user