Add jury assignment transfer, cap redistribution, and learning hub overhaul
All checks were successful
Build and Push Docker Image / build (push) Successful in 12m19s

- Add getTransferCandidates/transferAssignments procedures for targeted
  assignment moves between jurors with TOCTOU guards and audit logging
- Add getOverCapPreview/redistributeOverCap for auto-redistributing
  assignments when a juror's cap is lowered below their current load
- Add TransferAssignmentsDialog (2-step: select projects, pick destinations)
- Extend InlineMemberCap with over-cap detection and redistribute banner
- Extend getReassignmentHistory to show ASSIGNMENT_TRANSFER and CAP_REDISTRIBUTE events
- Learning hub: replace ResourceType/CohortLevel enums with accessJson JSONB,
  add coverImageKey, resource detail pages for jury/mentor, shared renderer
- Migration: 20260221200000_learning_hub_overhaul

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-02-21 18:50:29 +01:00
parent f42b452899
commit ee2f10e080
16 changed files with 2643 additions and 945 deletions

View File

@@ -1,38 +1,22 @@
'use client'
import { useState } from 'react'
import type { Route } from 'next'
import Link from 'next/link'
import { trpc } from '@/lib/trpc/client'
import {
Card,
CardContent,
} from '@/components/ui/card'
import { Badge } from '@/components/ui/badge'
import { Button } from '@/components/ui/button'
import { Skeleton } from '@/components/ui/skeleton'
import {
FileText,
Video,
Link as LinkIcon,
File,
Download,
ExternalLink,
BookOpen,
} from 'lucide-react'
const resourceTypeIcons = {
PDF: FileText,
VIDEO: Video,
DOCUMENT: File,
LINK: LinkIcon,
OTHER: File,
}
const cohortColors: Record<string, string> = {
ALL: 'bg-gray-100 text-gray-800',
SEMIFINALIST: 'bg-blue-100 text-blue-800',
FINALIST: 'bg-purple-100 text-purple-800',
}
export default function MentorResourcesPage() {
const [downloadingId, setDownloadingId] = useState<string | null>(null)
@@ -101,14 +85,14 @@ export default function MentorResourcesPage() {
) : (
<div className="grid gap-4">
{resources.map((resource) => {
const Icon = resourceTypeIcons[resource.resourceType as keyof typeof resourceTypeIcons] || File
const isDownloading = downloadingId === resource.id
const hasContent = !!resource.contentJson
return (
<Card key={resource.id}>
<CardContent className="flex items-center gap-4 py-4">
<div className="flex h-10 w-10 items-center justify-center rounded-lg bg-muted shrink-0">
<Icon className="h-5 w-5" />
<FileText className="h-5 w-5" />
</div>
<div className="flex-1 min-w-0">
<h3 className="font-medium">{resource.title}</h3>
@@ -117,36 +101,39 @@ export default function MentorResourcesPage() {
{resource.description}
</p>
)}
<div className="flex items-center gap-2 mt-2">
<Badge variant="outline" className={cohortColors[resource.cohortLevel] || cohortColors.ALL}>
{resource.cohortLevel}
</Badge>
<Badge variant="secondary">
{resource.resourceType}
</Badge>
</div>
</div>
<div>
{resource.externalUrl ? (
<div className="flex items-center gap-2">
{hasContent && (
<Link href={`/mentor/resources/${resource.id}` as Route}>
<Button variant="outline" size="sm">
<BookOpen className="mr-2 h-4 w-4" />
Read
</Button>
</Link>
)}
{resource.externalUrl && (
<a
href={resource.externalUrl}
target="_blank"
rel="noopener noreferrer"
>
<Button>
<Button variant="outline" size="sm">
<ExternalLink className="mr-2 h-4 w-4" />
Open
</Button>
</a>
) : resource.objectKey ? (
)}
{resource.objectKey && (
<Button
variant="outline"
size="sm"
onClick={() => handleDownload(resource.id)}
disabled={isDownloading}
>
<Download className="mr-2 h-4 w-4" />
{isDownloading ? 'Loading...' : 'Download'}
</Button>
) : null}
)}
</div>
</CardContent>
</Card>