Implement Prototype 1 improvements: unified members, project filters, audit expansion, filtering rounds, special awards
- Unified Member Management: merge /admin/users and /admin/mentors into /admin/members with role tabs, search, pagination
- Project List Filters: add search, multi-status filter, round/category/country selects, boolean toggles, URL persistence
- Audit Log Expansion: track logins, round state changes, evaluation submissions, file access, role changes via shared logAudit utility
- Founding Date Field: add foundedAt to Project model with CSV import support
- Filtering Round System: configurable rules (field-based, document check, AI screening), execution engine, results review with override/reinstate
- Special Awards System: named awards with eligibility criteria, dedicated jury, PICK_WINNER/RANKED/SCORED voting modes, AI eligibility
- Dashboard resilience: wrap heavy queries in try-catch to prevent error boundary on transient DB failures
- Reusable pagination component extracted to src/components/shared/pagination.tsx
- Old /admin/users and /admin/mentors routes redirect to /admin/members
- Prisma migration for all schema additions (additive, no data loss)
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-02-02 16:58:29 +01:00
|
|
|
'use client'
|
|
|
|
|
|
|
|
|
|
import { useState, useCallback, useEffect } from 'react'
|
|
|
|
|
import Link from 'next/link'
|
|
|
|
|
import { useSearchParams, usePathname } from 'next/navigation'
|
|
|
|
|
import { trpc } from '@/lib/trpc/client'
|
|
|
|
|
import { Badge } from '@/components/ui/badge'
|
|
|
|
|
import { Button } from '@/components/ui/button'
|
|
|
|
|
import { Input } from '@/components/ui/input'
|
|
|
|
|
import {
|
|
|
|
|
Card,
|
|
|
|
|
CardContent,
|
|
|
|
|
CardDescription,
|
|
|
|
|
CardHeader,
|
|
|
|
|
CardTitle,
|
|
|
|
|
} from '@/components/ui/card'
|
|
|
|
|
import {
|
|
|
|
|
Table,
|
|
|
|
|
TableBody,
|
|
|
|
|
TableCell,
|
|
|
|
|
TableHead,
|
|
|
|
|
TableHeader,
|
|
|
|
|
TableRow,
|
|
|
|
|
} from '@/components/ui/table'
|
|
|
|
|
import { Tabs, TabsList, TabsTrigger } from '@/components/ui/tabs'
|
|
|
|
|
import { Skeleton } from '@/components/ui/skeleton'
|
|
|
|
|
import { UserAvatar } from '@/components/shared/user-avatar'
|
|
|
|
|
import { UserActions, UserMobileActions } from '@/components/admin/user-actions'
|
|
|
|
|
import { Pagination } from '@/components/shared/pagination'
|
|
|
|
|
import { Plus, Users, Search } from 'lucide-react'
|
Comprehensive platform audit: security, UX, performance, and visual polish
Phase 1: Security - status transition validation, Zod tightening, DB indexes, transactions
Phase 2: Admin UX - search/filter for awards, learning, partners pages
Phase 3: Dashboard - Recent Activity feed, Pending Actions card, quick actions
Phase 4: Jury - assignments progress/urgency, autosave indicator, divergence highlighting
Phase 5: Portals - observer charts, mentor search, login/onboarding polish
Phase 6: Messages preview dialog, CsvExportDialog with column selection
Phase 7: Performance - query optimizations, loading skeletons, useDebounce hook
Phase 8: Visual - AnimatedCard, hover effects, StatusBadge, empty state CTAs
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-08 22:05:01 +01:00
|
|
|
import { formatRelativeTime } from '@/lib/utils'
|
Implement Prototype 1 improvements: unified members, project filters, audit expansion, filtering rounds, special awards
- Unified Member Management: merge /admin/users and /admin/mentors into /admin/members with role tabs, search, pagination
- Project List Filters: add search, multi-status filter, round/category/country selects, boolean toggles, URL persistence
- Audit Log Expansion: track logins, round state changes, evaluation submissions, file access, role changes via shared logAudit utility
- Founding Date Field: add foundedAt to Project model with CSV import support
- Filtering Round System: configurable rules (field-based, document check, AI screening), execution engine, results review with override/reinstate
- Special Awards System: named awards with eligibility criteria, dedicated jury, PICK_WINNER/RANKED/SCORED voting modes, AI eligibility
- Dashboard resilience: wrap heavy queries in try-catch to prevent error boundary on transient DB failures
- Reusable pagination component extracted to src/components/shared/pagination.tsx
- Old /admin/users and /admin/mentors routes redirect to /admin/members
- Prisma migration for all schema additions (additive, no data loss)
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-02-02 16:58:29 +01:00
|
|
|
type RoleValue = 'SUPER_ADMIN' | 'PROGRAM_ADMIN' | 'JURY_MEMBER' | 'MENTOR' | 'OBSERVER'
|
|
|
|
|
|
|
|
|
|
type TabKey = 'all' | 'jury' | 'mentors' | 'observers' | 'admins'
|
|
|
|
|
|
|
|
|
|
const TAB_ROLES: Record<TabKey, RoleValue[] | undefined> = {
|
|
|
|
|
all: undefined,
|
|
|
|
|
jury: ['JURY_MEMBER'],
|
|
|
|
|
mentors: ['MENTOR'],
|
|
|
|
|
observers: ['OBSERVER'],
|
|
|
|
|
admins: ['SUPER_ADMIN', 'PROGRAM_ADMIN'],
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const statusColors: Record<string, 'default' | 'success' | 'secondary' | 'destructive'> = {
|
|
|
|
|
ACTIVE: 'success',
|
|
|
|
|
INVITED: 'secondary',
|
|
|
|
|
SUSPENDED: 'destructive',
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const roleColors: Record<string, 'default' | 'outline' | 'secondary'> = {
|
|
|
|
|
JURY_MEMBER: 'default',
|
|
|
|
|
MENTOR: 'secondary',
|
|
|
|
|
OBSERVER: 'outline',
|
|
|
|
|
PROGRAM_ADMIN: 'default',
|
|
|
|
|
SUPER_ADMIN: 'destructive' as 'default',
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
export function MembersContent() {
|
|
|
|
|
const searchParams = useSearchParams()
|
|
|
|
|
|
|
|
|
|
const pathname = usePathname()
|
|
|
|
|
|
|
|
|
|
const tab = (searchParams.get('tab') as TabKey) || 'all'
|
|
|
|
|
const search = searchParams.get('search') || ''
|
|
|
|
|
const page = parseInt(searchParams.get('page') || '1', 10)
|
|
|
|
|
|
|
|
|
|
const [searchInput, setSearchInput] = useState(search)
|
|
|
|
|
|
|
|
|
|
// Debounced search
|
|
|
|
|
useEffect(() => {
|
|
|
|
|
const timer = setTimeout(() => {
|
|
|
|
|
updateParams({ search: searchInput || null, page: '1' })
|
|
|
|
|
}, 300)
|
|
|
|
|
return () => clearTimeout(timer)
|
|
|
|
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
|
|
|
}, [searchInput])
|
|
|
|
|
|
|
|
|
|
const updateParams = useCallback(
|
|
|
|
|
(updates: Record<string, string | null>) => {
|
|
|
|
|
const params = new URLSearchParams(searchParams.toString())
|
|
|
|
|
Object.entries(updates).forEach(([key, value]) => {
|
|
|
|
|
if (value === null || value === '') {
|
|
|
|
|
params.delete(key)
|
|
|
|
|
} else {
|
|
|
|
|
params.set(key, value)
|
|
|
|
|
}
|
|
|
|
|
})
|
|
|
|
|
window.history.replaceState(null, '', `${pathname}?${params.toString()}`)
|
|
|
|
|
},
|
|
|
|
|
[searchParams, pathname]
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
const roles = TAB_ROLES[tab]
|
|
|
|
|
|
|
|
|
|
const { data, isLoading } = trpc.user.list.useQuery({
|
|
|
|
|
roles: roles,
|
|
|
|
|
search: search || undefined,
|
|
|
|
|
page,
|
|
|
|
|
perPage: 20,
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
const handleTabChange = (value: string) => {
|
|
|
|
|
updateParams({ tab: value === 'all' ? null : value, page: '1' })
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return (
|
|
|
|
|
<div className="space-y-6">
|
|
|
|
|
{/* Header */}
|
|
|
|
|
<div className="flex items-center justify-between">
|
|
|
|
|
<div>
|
|
|
|
|
<h1 className="text-2xl font-semibold tracking-tight">Members</h1>
|
|
|
|
|
<p className="text-muted-foreground">
|
|
|
|
|
Manage jury members, mentors, observers, and admins
|
|
|
|
|
</p>
|
|
|
|
|
</div>
|
|
|
|
|
<Button asChild>
|
|
|
|
|
<Link href="/admin/members/invite">
|
|
|
|
|
<Plus className="mr-2 h-4 w-4" />
|
|
|
|
|
Invite Member
|
|
|
|
|
</Link>
|
|
|
|
|
</Button>
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
{/* Tabs */}
|
|
|
|
|
<Tabs value={tab} onValueChange={handleTabChange}>
|
|
|
|
|
<div className="flex flex-col gap-4 sm:flex-row sm:items-center sm:justify-between">
|
|
|
|
|
<TabsList>
|
|
|
|
|
<TabsTrigger value="all">All</TabsTrigger>
|
|
|
|
|
<TabsTrigger value="jury">Jury</TabsTrigger>
|
|
|
|
|
<TabsTrigger value="mentors">Mentors</TabsTrigger>
|
|
|
|
|
<TabsTrigger value="observers">Observers</TabsTrigger>
|
|
|
|
|
<TabsTrigger value="admins">Admins</TabsTrigger>
|
|
|
|
|
</TabsList>
|
|
|
|
|
|
|
|
|
|
{/* Search */}
|
|
|
|
|
<div className="relative w-full sm:w-64">
|
|
|
|
|
<Search className="absolute left-3 top-1/2 h-4 w-4 -translate-y-1/2 text-muted-foreground" />
|
|
|
|
|
<Input
|
|
|
|
|
placeholder="Search by name or email..."
|
|
|
|
|
value={searchInput}
|
|
|
|
|
onChange={(e) => setSearchInput(e.target.value)}
|
|
|
|
|
className="pl-9"
|
|
|
|
|
/>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
</Tabs>
|
|
|
|
|
|
|
|
|
|
{/* Content */}
|
|
|
|
|
{isLoading ? (
|
|
|
|
|
<MembersSkeleton />
|
|
|
|
|
) : data && data.users.length > 0 ? (
|
|
|
|
|
<>
|
|
|
|
|
{/* Desktop table */}
|
|
|
|
|
<Card className="hidden md:block">
|
|
|
|
|
<Table>
|
|
|
|
|
<TableHeader>
|
|
|
|
|
<TableRow>
|
|
|
|
|
<TableHead>Member</TableHead>
|
|
|
|
|
<TableHead>Role</TableHead>
|
|
|
|
|
<TableHead>Expertise</TableHead>
|
|
|
|
|
<TableHead>Assignments</TableHead>
|
|
|
|
|
<TableHead>Status</TableHead>
|
|
|
|
|
<TableHead>Last Login</TableHead>
|
|
|
|
|
<TableHead className="text-right">Actions</TableHead>
|
|
|
|
|
</TableRow>
|
|
|
|
|
</TableHeader>
|
|
|
|
|
<TableBody>
|
|
|
|
|
{data.users.map((user) => (
|
|
|
|
|
<TableRow key={user.id}>
|
|
|
|
|
<TableCell>
|
|
|
|
|
<div className="flex items-center gap-3">
|
|
|
|
|
<UserAvatar
|
|
|
|
|
user={user}
|
|
|
|
|
avatarUrl={(user as Record<string, unknown>).avatarUrl as string | undefined}
|
|
|
|
|
size="sm"
|
|
|
|
|
/>
|
|
|
|
|
<div>
|
|
|
|
|
<p className="font-medium">{user.name || 'Unnamed'}</p>
|
|
|
|
|
<p className="text-sm text-muted-foreground">
|
|
|
|
|
{user.email}
|
|
|
|
|
</p>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
</TableCell>
|
|
|
|
|
<TableCell>
|
|
|
|
|
<Badge variant={roleColors[user.role] || 'secondary'}>
|
|
|
|
|
{user.role.replace(/_/g, ' ')}
|
|
|
|
|
</Badge>
|
|
|
|
|
</TableCell>
|
|
|
|
|
<TableCell>
|
|
|
|
|
{user.expertiseTags && user.expertiseTags.length > 0 ? (
|
|
|
|
|
<div className="flex flex-wrap gap-1">
|
|
|
|
|
{user.expertiseTags.slice(0, 2).map((tag) => (
|
|
|
|
|
<Badge key={tag} variant="outline" className="text-xs">
|
|
|
|
|
{tag}
|
|
|
|
|
</Badge>
|
|
|
|
|
))}
|
|
|
|
|
{user.expertiseTags.length > 2 && (
|
|
|
|
|
<Badge variant="outline" className="text-xs">
|
|
|
|
|
+{user.expertiseTags.length - 2}
|
|
|
|
|
</Badge>
|
|
|
|
|
)}
|
|
|
|
|
</div>
|
|
|
|
|
) : (
|
|
|
|
|
<span className="text-sm text-muted-foreground">-</span>
|
|
|
|
|
)}
|
|
|
|
|
</TableCell>
|
|
|
|
|
<TableCell>
|
|
|
|
|
<div>
|
|
|
|
|
{user.role === 'MENTOR' ? (
|
Fix build errors: add missing Prisma models/fields and resolve TypeScript type errors
Schema: Add 11 new models (RoundTemplate, MentorNote, MentorMilestone,
MentorMilestoneCompletion, EvaluationDiscussion, DiscussionComment,
Message, MessageRecipient, MessageTemplate, Webhook, WebhookDelivery,
DigestLog) and missing fields on existing models (Project.isDraft,
ProjectFile.version, LiveVotingSession.allowAudienceVotes, User.digestFrequency,
AuditLog.sessionId, MentorAssignment.completionStatus, etc).
Add AUDIT_CONFIG/LOCALIZATION/DIGEST/ANALYTICS enum values.
Code: Fix implicit any types, route type casts, enum casts, null safety,
composite key handling, and relation field names across 11 source files.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-08 14:04:02 +01:00
|
|
|
<p>{(user as unknown as { _count: { mentorAssignments: number; assignments: number } })._count.mentorAssignments} mentored</p>
|
Implement Prototype 1 improvements: unified members, project filters, audit expansion, filtering rounds, special awards
- Unified Member Management: merge /admin/users and /admin/mentors into /admin/members with role tabs, search, pagination
- Project List Filters: add search, multi-status filter, round/category/country selects, boolean toggles, URL persistence
- Audit Log Expansion: track logins, round state changes, evaluation submissions, file access, role changes via shared logAudit utility
- Founding Date Field: add foundedAt to Project model with CSV import support
- Filtering Round System: configurable rules (field-based, document check, AI screening), execution engine, results review with override/reinstate
- Special Awards System: named awards with eligibility criteria, dedicated jury, PICK_WINNER/RANKED/SCORED voting modes, AI eligibility
- Dashboard resilience: wrap heavy queries in try-catch to prevent error boundary on transient DB failures
- Reusable pagination component extracted to src/components/shared/pagination.tsx
- Old /admin/users and /admin/mentors routes redirect to /admin/members
- Prisma migration for all schema additions (additive, no data loss)
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-02-02 16:58:29 +01:00
|
|
|
) : (
|
Fix build errors: add missing Prisma models/fields and resolve TypeScript type errors
Schema: Add 11 new models (RoundTemplate, MentorNote, MentorMilestone,
MentorMilestoneCompletion, EvaluationDiscussion, DiscussionComment,
Message, MessageRecipient, MessageTemplate, Webhook, WebhookDelivery,
DigestLog) and missing fields on existing models (Project.isDraft,
ProjectFile.version, LiveVotingSession.allowAudienceVotes, User.digestFrequency,
AuditLog.sessionId, MentorAssignment.completionStatus, etc).
Add AUDIT_CONFIG/LOCALIZATION/DIGEST/ANALYTICS enum values.
Code: Fix implicit any types, route type casts, enum casts, null safety,
composite key handling, and relation field names across 11 source files.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-08 14:04:02 +01:00
|
|
|
<p>{(user as unknown as { _count: { mentorAssignments: number; assignments: number } })._count.assignments} assigned</p>
|
Implement Prototype 1 improvements: unified members, project filters, audit expansion, filtering rounds, special awards
- Unified Member Management: merge /admin/users and /admin/mentors into /admin/members with role tabs, search, pagination
- Project List Filters: add search, multi-status filter, round/category/country selects, boolean toggles, URL persistence
- Audit Log Expansion: track logins, round state changes, evaluation submissions, file access, role changes via shared logAudit utility
- Founding Date Field: add foundedAt to Project model with CSV import support
- Filtering Round System: configurable rules (field-based, document check, AI screening), execution engine, results review with override/reinstate
- Special Awards System: named awards with eligibility criteria, dedicated jury, PICK_WINNER/RANKED/SCORED voting modes, AI eligibility
- Dashboard resilience: wrap heavy queries in try-catch to prevent error boundary on transient DB failures
- Reusable pagination component extracted to src/components/shared/pagination.tsx
- Old /admin/users and /admin/mentors routes redirect to /admin/members
- Prisma migration for all schema additions (additive, no data loss)
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-02-02 16:58:29 +01:00
|
|
|
)}
|
|
|
|
|
</div>
|
|
|
|
|
</TableCell>
|
|
|
|
|
<TableCell>
|
|
|
|
|
<Badge variant={statusColors[user.status] || 'secondary'}>
|
|
|
|
|
{user.status}
|
|
|
|
|
</Badge>
|
|
|
|
|
</TableCell>
|
|
|
|
|
<TableCell>
|
|
|
|
|
{user.lastLoginAt ? (
|
Comprehensive platform audit: security, UX, performance, and visual polish
Phase 1: Security - status transition validation, Zod tightening, DB indexes, transactions
Phase 2: Admin UX - search/filter for awards, learning, partners pages
Phase 3: Dashboard - Recent Activity feed, Pending Actions card, quick actions
Phase 4: Jury - assignments progress/urgency, autosave indicator, divergence highlighting
Phase 5: Portals - observer charts, mentor search, login/onboarding polish
Phase 6: Messages preview dialog, CsvExportDialog with column selection
Phase 7: Performance - query optimizations, loading skeletons, useDebounce hook
Phase 8: Visual - AnimatedCard, hover effects, StatusBadge, empty state CTAs
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-08 22:05:01 +01:00
|
|
|
<span title={new Date(user.lastLoginAt).toLocaleString()}>
|
|
|
|
|
{formatRelativeTime(user.lastLoginAt)}
|
|
|
|
|
</span>
|
Implement Prototype 1 improvements: unified members, project filters, audit expansion, filtering rounds, special awards
- Unified Member Management: merge /admin/users and /admin/mentors into /admin/members with role tabs, search, pagination
- Project List Filters: add search, multi-status filter, round/category/country selects, boolean toggles, URL persistence
- Audit Log Expansion: track logins, round state changes, evaluation submissions, file access, role changes via shared logAudit utility
- Founding Date Field: add foundedAt to Project model with CSV import support
- Filtering Round System: configurable rules (field-based, document check, AI screening), execution engine, results review with override/reinstate
- Special Awards System: named awards with eligibility criteria, dedicated jury, PICK_WINNER/RANKED/SCORED voting modes, AI eligibility
- Dashboard resilience: wrap heavy queries in try-catch to prevent error boundary on transient DB failures
- Reusable pagination component extracted to src/components/shared/pagination.tsx
- Old /admin/users and /admin/mentors routes redirect to /admin/members
- Prisma migration for all schema additions (additive, no data loss)
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-02-02 16:58:29 +01:00
|
|
|
) : (
|
|
|
|
|
<span className="text-muted-foreground">Never</span>
|
|
|
|
|
)}
|
|
|
|
|
</TableCell>
|
|
|
|
|
<TableCell className="text-right">
|
|
|
|
|
<UserActions
|
|
|
|
|
userId={user.id}
|
|
|
|
|
userEmail={user.email}
|
|
|
|
|
userStatus={user.status}
|
|
|
|
|
/>
|
|
|
|
|
</TableCell>
|
|
|
|
|
</TableRow>
|
|
|
|
|
))}
|
|
|
|
|
</TableBody>
|
|
|
|
|
</Table>
|
|
|
|
|
</Card>
|
|
|
|
|
|
|
|
|
|
{/* Mobile cards */}
|
|
|
|
|
<div className="space-y-4 md:hidden">
|
|
|
|
|
{data.users.map((user) => (
|
|
|
|
|
<Card key={user.id}>
|
|
|
|
|
<CardHeader className="pb-3">
|
|
|
|
|
<div className="flex items-start justify-between">
|
|
|
|
|
<div className="flex items-center gap-3">
|
|
|
|
|
<UserAvatar
|
|
|
|
|
user={user}
|
|
|
|
|
avatarUrl={(user as Record<string, unknown>).avatarUrl as string | undefined}
|
|
|
|
|
size="md"
|
|
|
|
|
/>
|
|
|
|
|
<div>
|
|
|
|
|
<CardTitle className="text-base">
|
|
|
|
|
{user.name || 'Unnamed'}
|
|
|
|
|
</CardTitle>
|
|
|
|
|
<CardDescription className="text-xs">
|
|
|
|
|
{user.email}
|
|
|
|
|
</CardDescription>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
<Badge variant={statusColors[user.status] || 'secondary'}>
|
|
|
|
|
{user.status}
|
|
|
|
|
</Badge>
|
|
|
|
|
</div>
|
|
|
|
|
</CardHeader>
|
|
|
|
|
<CardContent className="space-y-3">
|
|
|
|
|
<div className="flex items-center justify-between text-sm">
|
|
|
|
|
<span className="text-muted-foreground">Role</span>
|
|
|
|
|
<Badge variant={roleColors[user.role] || 'secondary'}>
|
|
|
|
|
{user.role.replace(/_/g, ' ')}
|
|
|
|
|
</Badge>
|
|
|
|
|
</div>
|
|
|
|
|
<div className="flex items-center justify-between text-sm">
|
|
|
|
|
<span className="text-muted-foreground">Assignments</span>
|
|
|
|
|
<span>
|
|
|
|
|
{user.role === 'MENTOR'
|
Fix build errors: add missing Prisma models/fields and resolve TypeScript type errors
Schema: Add 11 new models (RoundTemplate, MentorNote, MentorMilestone,
MentorMilestoneCompletion, EvaluationDiscussion, DiscussionComment,
Message, MessageRecipient, MessageTemplate, Webhook, WebhookDelivery,
DigestLog) and missing fields on existing models (Project.isDraft,
ProjectFile.version, LiveVotingSession.allowAudienceVotes, User.digestFrequency,
AuditLog.sessionId, MentorAssignment.completionStatus, etc).
Add AUDIT_CONFIG/LOCALIZATION/DIGEST/ANALYTICS enum values.
Code: Fix implicit any types, route type casts, enum casts, null safety,
composite key handling, and relation field names across 11 source files.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-08 14:04:02 +01:00
|
|
|
? `${(user as unknown as { _count: { mentorAssignments: number; assignments: number } })._count.mentorAssignments} mentored`
|
|
|
|
|
: `${(user as unknown as { _count: { mentorAssignments: number; assignments: number } })._count.assignments} assigned`}
|
Implement Prototype 1 improvements: unified members, project filters, audit expansion, filtering rounds, special awards
- Unified Member Management: merge /admin/users and /admin/mentors into /admin/members with role tabs, search, pagination
- Project List Filters: add search, multi-status filter, round/category/country selects, boolean toggles, URL persistence
- Audit Log Expansion: track logins, round state changes, evaluation submissions, file access, role changes via shared logAudit utility
- Founding Date Field: add foundedAt to Project model with CSV import support
- Filtering Round System: configurable rules (field-based, document check, AI screening), execution engine, results review with override/reinstate
- Special Awards System: named awards with eligibility criteria, dedicated jury, PICK_WINNER/RANKED/SCORED voting modes, AI eligibility
- Dashboard resilience: wrap heavy queries in try-catch to prevent error boundary on transient DB failures
- Reusable pagination component extracted to src/components/shared/pagination.tsx
- Old /admin/users and /admin/mentors routes redirect to /admin/members
- Prisma migration for all schema additions (additive, no data loss)
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-02-02 16:58:29 +01:00
|
|
|
</span>
|
|
|
|
|
</div>
|
Comprehensive platform audit: security, UX, performance, and visual polish
Phase 1: Security - status transition validation, Zod tightening, DB indexes, transactions
Phase 2: Admin UX - search/filter for awards, learning, partners pages
Phase 3: Dashboard - Recent Activity feed, Pending Actions card, quick actions
Phase 4: Jury - assignments progress/urgency, autosave indicator, divergence highlighting
Phase 5: Portals - observer charts, mentor search, login/onboarding polish
Phase 6: Messages preview dialog, CsvExportDialog with column selection
Phase 7: Performance - query optimizations, loading skeletons, useDebounce hook
Phase 8: Visual - AnimatedCard, hover effects, StatusBadge, empty state CTAs
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-08 22:05:01 +01:00
|
|
|
<div className="flex items-center justify-between text-sm">
|
|
|
|
|
<span className="text-muted-foreground">Last Login</span>
|
|
|
|
|
<span>
|
|
|
|
|
{user.lastLoginAt ? (
|
|
|
|
|
formatRelativeTime(user.lastLoginAt)
|
|
|
|
|
) : (
|
|
|
|
|
<span className="text-muted-foreground">Never</span>
|
|
|
|
|
)}
|
|
|
|
|
</span>
|
|
|
|
|
</div>
|
Implement Prototype 1 improvements: unified members, project filters, audit expansion, filtering rounds, special awards
- Unified Member Management: merge /admin/users and /admin/mentors into /admin/members with role tabs, search, pagination
- Project List Filters: add search, multi-status filter, round/category/country selects, boolean toggles, URL persistence
- Audit Log Expansion: track logins, round state changes, evaluation submissions, file access, role changes via shared logAudit utility
- Founding Date Field: add foundedAt to Project model with CSV import support
- Filtering Round System: configurable rules (field-based, document check, AI screening), execution engine, results review with override/reinstate
- Special Awards System: named awards with eligibility criteria, dedicated jury, PICK_WINNER/RANKED/SCORED voting modes, AI eligibility
- Dashboard resilience: wrap heavy queries in try-catch to prevent error boundary on transient DB failures
- Reusable pagination component extracted to src/components/shared/pagination.tsx
- Old /admin/users and /admin/mentors routes redirect to /admin/members
- Prisma migration for all schema additions (additive, no data loss)
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-02-02 16:58:29 +01:00
|
|
|
{user.expertiseTags && user.expertiseTags.length > 0 && (
|
|
|
|
|
<div className="flex flex-wrap gap-1">
|
|
|
|
|
{user.expertiseTags.map((tag) => (
|
|
|
|
|
<Badge key={tag} variant="outline" className="text-xs">
|
|
|
|
|
{tag}
|
|
|
|
|
</Badge>
|
|
|
|
|
))}
|
|
|
|
|
</div>
|
|
|
|
|
)}
|
|
|
|
|
<UserMobileActions
|
|
|
|
|
userId={user.id}
|
|
|
|
|
userEmail={user.email}
|
|
|
|
|
userStatus={user.status}
|
|
|
|
|
/>
|
|
|
|
|
</CardContent>
|
|
|
|
|
</Card>
|
|
|
|
|
))}
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
{/* Pagination */}
|
|
|
|
|
<Pagination
|
|
|
|
|
page={page}
|
|
|
|
|
totalPages={data.totalPages}
|
|
|
|
|
total={data.total}
|
|
|
|
|
perPage={data.perPage}
|
|
|
|
|
onPageChange={(newPage) => updateParams({ page: String(newPage) })}
|
|
|
|
|
/>
|
|
|
|
|
</>
|
|
|
|
|
) : (
|
|
|
|
|
<Card>
|
|
|
|
|
<CardContent className="flex flex-col items-center justify-center py-12 text-center">
|
|
|
|
|
<Users className="h-12 w-12 text-muted-foreground/50" />
|
|
|
|
|
<p className="mt-2 font-medium">No members found</p>
|
|
|
|
|
<p className="text-sm text-muted-foreground">
|
|
|
|
|
{search
|
|
|
|
|
? 'Try adjusting your search'
|
|
|
|
|
: 'Invite members to get started'}
|
|
|
|
|
</p>
|
|
|
|
|
<Button asChild className="mt-4">
|
|
|
|
|
<Link href="/admin/members/invite">
|
|
|
|
|
<Plus className="mr-2 h-4 w-4" />
|
|
|
|
|
Invite Member
|
|
|
|
|
</Link>
|
|
|
|
|
</Button>
|
|
|
|
|
</CardContent>
|
|
|
|
|
</Card>
|
|
|
|
|
)}
|
|
|
|
|
</div>
|
|
|
|
|
)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function MembersSkeleton() {
|
|
|
|
|
return (
|
|
|
|
|
<Card>
|
|
|
|
|
<CardContent className="p-6">
|
|
|
|
|
<div className="space-y-4">
|
|
|
|
|
{[...Array(5)].map((_, i) => (
|
|
|
|
|
<div key={i} className="flex items-center gap-4">
|
|
|
|
|
<Skeleton className="h-10 w-10 rounded-full" />
|
|
|
|
|
<div className="flex-1 space-y-2">
|
|
|
|
|
<Skeleton className="h-5 w-32" />
|
|
|
|
|
<Skeleton className="h-4 w-48" />
|
|
|
|
|
</div>
|
|
|
|
|
<Skeleton className="h-9 w-9" />
|
|
|
|
|
</div>
|
|
|
|
|
))}
|
|
|
|
|
</div>
|
|
|
|
|
</CardContent>
|
|
|
|
|
</Card>
|
|
|
|
|
)
|
|
|
|
|
}
|