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>
This commit is contained in:
56
src/components/shared/pagination.tsx
Normal file
56
src/components/shared/pagination.tsx
Normal file
@@ -0,0 +1,56 @@
|
||||
'use client'
|
||||
|
||||
import { Button } from '@/components/ui/button'
|
||||
import { ChevronLeft, ChevronRight } from 'lucide-react'
|
||||
|
||||
interface PaginationProps {
|
||||
page: number
|
||||
totalPages: number
|
||||
total: number
|
||||
perPage: number
|
||||
onPageChange: (page: number) => void
|
||||
}
|
||||
|
||||
export function Pagination({
|
||||
page,
|
||||
totalPages,
|
||||
total,
|
||||
perPage,
|
||||
onPageChange,
|
||||
}: PaginationProps) {
|
||||
if (totalPages <= 1) return null
|
||||
|
||||
const from = (page - 1) * perPage + 1
|
||||
const to = Math.min(page * perPage, total)
|
||||
|
||||
return (
|
||||
<div className="flex items-center justify-between">
|
||||
<p className="text-sm text-muted-foreground">
|
||||
Showing {from} to {to} of {total} results
|
||||
</p>
|
||||
<div className="flex items-center gap-2">
|
||||
<Button
|
||||
variant="outline"
|
||||
size="sm"
|
||||
onClick={() => onPageChange(page - 1)}
|
||||
disabled={page === 1}
|
||||
>
|
||||
<ChevronLeft className="h-4 w-4" />
|
||||
Previous
|
||||
</Button>
|
||||
<span className="text-sm">
|
||||
Page {page} of {totalPages}
|
||||
</span>
|
||||
<Button
|
||||
variant="outline"
|
||||
size="sm"
|
||||
onClick={() => onPageChange(page + 1)}
|
||||
disabled={page >= totalPages}
|
||||
>
|
||||
Next
|
||||
<ChevronRight className="h-4 w-4" />
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
111
src/components/shared/user-activity-log.tsx
Normal file
111
src/components/shared/user-activity-log.tsx
Normal file
@@ -0,0 +1,111 @@
|
||||
'use client'
|
||||
|
||||
import { trpc } from '@/lib/trpc/client'
|
||||
import { Badge } from '@/components/ui/badge'
|
||||
import {
|
||||
Card,
|
||||
CardContent,
|
||||
CardDescription,
|
||||
CardHeader,
|
||||
CardTitle,
|
||||
} from '@/components/ui/card'
|
||||
import { Skeleton } from '@/components/ui/skeleton'
|
||||
import { Clock, Activity } from 'lucide-react'
|
||||
import { formatDate } from '@/lib/utils'
|
||||
|
||||
const actionColors: Record<string, 'default' | 'destructive' | 'secondary' | 'outline'> = {
|
||||
CREATE: 'default',
|
||||
UPDATE: 'secondary',
|
||||
DELETE: 'destructive',
|
||||
LOGIN_SUCCESS: 'outline',
|
||||
LOGIN_FAILED: 'destructive',
|
||||
INVITATION_ACCEPTED: 'default',
|
||||
EVALUATION_SUBMITTED: 'default',
|
||||
SUBMIT_EVALUATION: 'default',
|
||||
ROLE_CHANGED: 'secondary',
|
||||
PASSWORD_SET: 'outline',
|
||||
PASSWORD_CHANGED: 'outline',
|
||||
FILE_DOWNLOADED: 'outline',
|
||||
ROUND_ACTIVATED: 'default',
|
||||
ROUND_CLOSED: 'secondary',
|
||||
}
|
||||
|
||||
interface UserActivityLogProps {
|
||||
userId: string
|
||||
limit?: number
|
||||
}
|
||||
|
||||
export function UserActivityLog({ userId, limit = 20 }: UserActivityLogProps) {
|
||||
const { data: logs, isLoading } = trpc.audit.getByUser.useQuery({
|
||||
userId,
|
||||
limit,
|
||||
})
|
||||
|
||||
if (isLoading) {
|
||||
return (
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle>Activity Log</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<div className="space-y-3">
|
||||
{[...Array(5)].map((_, i) => (
|
||||
<div key={i} className="flex items-center gap-3">
|
||||
<Skeleton className="h-4 w-32" />
|
||||
<Skeleton className="h-6 w-20" />
|
||||
<Skeleton className="h-4 w-24" />
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
)
|
||||
}
|
||||
|
||||
return (
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle className="flex items-center gap-2">
|
||||
<Activity className="h-5 w-5" />
|
||||
Activity Log
|
||||
</CardTitle>
|
||||
<CardDescription>Recent actions by this member</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
{logs && logs.length > 0 ? (
|
||||
<div className="space-y-3">
|
||||
{logs.map((log) => (
|
||||
<div
|
||||
key={log.id}
|
||||
className="flex items-center gap-3 text-sm"
|
||||
>
|
||||
<div className="flex items-center gap-1 text-muted-foreground shrink-0 w-36">
|
||||
<Clock className="h-3 w-3" />
|
||||
<span className="text-xs font-mono">
|
||||
{formatDate(log.timestamp)}
|
||||
</span>
|
||||
</div>
|
||||
<Badge
|
||||
variant={actionColors[log.action] || 'secondary'}
|
||||
className="shrink-0"
|
||||
>
|
||||
{log.action.replace(/_/g, ' ')}
|
||||
</Badge>
|
||||
<span className="text-muted-foreground truncate">
|
||||
{log.entityType}
|
||||
{log.entityId && (
|
||||
<span className="font-mono text-xs ml-1">
|
||||
{log.entityId.slice(0, 8)}...
|
||||
</span>
|
||||
)}
|
||||
</span>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
) : (
|
||||
<p className="text-sm text-muted-foreground">No activity recorded yet.</p>
|
||||
)}
|
||||
</CardContent>
|
||||
</Card>
|
||||
)
|
||||
}
|
||||
Reference in New Issue
Block a user