Competition/Round architecture: full platform rewrite (Phases 1-9)
All checks were successful
Build and Push Docker Image / build (push) Successful in 7m45s
All checks were successful
Build and Push Docker Image / build (push) Successful in 7m45s
Replace Pipeline/Stage system with Competition/Round architecture. New schema: Competition, Round (7 types), JuryGroup, AssignmentPolicy, ProjectRoundState, DeliberationSession, ResultLock, SubmissionWindow. New services: round-engine, round-assignment, deliberation, result-lock, submission-manager, competition-context, ai-prompt-guard. Full admin/jury/applicant/mentor UI rewrite. AI prompt hardening with structured prompts, retry logic, and injection detection. All legacy pipeline/stage code removed. 4 new migrations + seed aligned. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
181
src/components/admin/assignment/assignment-preview-sheet.tsx
Normal file
181
src/components/admin/assignment/assignment-preview-sheet.tsx
Normal file
@@ -0,0 +1,181 @@
|
||||
'use client'
|
||||
|
||||
import { useState, useEffect } from 'react'
|
||||
import { AlertTriangle, Bot, CheckCircle2 } from 'lucide-react'
|
||||
import { toast } from 'sonner'
|
||||
import { trpc } from '@/lib/trpc/client'
|
||||
import {
|
||||
Sheet,
|
||||
SheetContent,
|
||||
SheetDescription,
|
||||
SheetFooter,
|
||||
SheetHeader,
|
||||
SheetTitle,
|
||||
} from '@/components/ui/sheet'
|
||||
import { Button } from '@/components/ui/button'
|
||||
import { ScrollArea } from '@/components/ui/scroll-area'
|
||||
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card'
|
||||
import { Badge } from '@/components/ui/badge'
|
||||
import { Skeleton } from '@/components/ui/skeleton'
|
||||
|
||||
interface AssignmentPreviewSheetProps {
|
||||
roundId: string
|
||||
open: boolean
|
||||
onOpenChange: (open: boolean) => void
|
||||
}
|
||||
|
||||
export function AssignmentPreviewSheet({
|
||||
roundId,
|
||||
open,
|
||||
onOpenChange,
|
||||
}: AssignmentPreviewSheetProps) {
|
||||
const utils = trpc.useUtils()
|
||||
|
||||
const {
|
||||
data: preview,
|
||||
isLoading,
|
||||
refetch,
|
||||
} = trpc.roundAssignment.preview.useQuery(
|
||||
{ roundId, honorIntents: true, requiredReviews: 3 },
|
||||
{ enabled: open }
|
||||
)
|
||||
|
||||
const { mutate: execute, isPending: isExecuting } = trpc.roundAssignment.execute.useMutation({
|
||||
onSuccess: (result) => {
|
||||
toast.success(`Created ${result.created} assignments`)
|
||||
utils.roundAssignment.coverageReport.invalidate({ roundId })
|
||||
utils.roundAssignment.unassignedQueue.invalidate({ roundId })
|
||||
onOpenChange(false)
|
||||
},
|
||||
onError: (err) => {
|
||||
toast.error(err.message)
|
||||
},
|
||||
})
|
||||
|
||||
useEffect(() => {
|
||||
if (open) {
|
||||
refetch()
|
||||
}
|
||||
}, [open, refetch])
|
||||
|
||||
const handleExecute = () => {
|
||||
if (!preview?.assignments || preview.assignments.length === 0) {
|
||||
toast.error('No assignments to execute')
|
||||
return
|
||||
}
|
||||
|
||||
execute({
|
||||
roundId,
|
||||
assignments: preview.assignments.map((a: any) => ({
|
||||
userId: a.userId,
|
||||
projectId: a.projectId,
|
||||
})),
|
||||
})
|
||||
}
|
||||
|
||||
return (
|
||||
<Sheet open={open} onOpenChange={onOpenChange}>
|
||||
<SheetContent className="w-full sm:max-w-xl">
|
||||
<SheetHeader>
|
||||
<SheetTitle>Assignment Preview</SheetTitle>
|
||||
<SheetDescription className="flex items-center gap-2">
|
||||
<Badge variant="outline" className="text-xs gap-1 shrink-0">
|
||||
<Bot className="h-3 w-3" />
|
||||
AI Suggested
|
||||
</Badge>
|
||||
Review the proposed assignments before executing. All assignments are admin-approved on execute.
|
||||
</SheetDescription>
|
||||
</SheetHeader>
|
||||
|
||||
<ScrollArea className="h-[calc(100vh-200px)] mt-6">
|
||||
{isLoading ? (
|
||||
<div className="space-y-3">
|
||||
{[1, 2, 3].map((i) => (
|
||||
<Skeleton key={i} className="h-20 w-full" />
|
||||
))}
|
||||
</div>
|
||||
) : preview ? (
|
||||
<div className="space-y-4">
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle className="text-sm flex items-center gap-2">
|
||||
<CheckCircle2 className="h-4 w-4 text-green-600" />
|
||||
{preview.stats.assignmentsGenerated || 0} Assignments Proposed
|
||||
</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<p className="text-sm text-muted-foreground">
|
||||
{preview.stats.totalJurors || 0} jurors will receive assignments
|
||||
</p>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
{preview.warnings && preview.warnings.length > 0 && (
|
||||
<Card className="border-amber-500">
|
||||
<CardHeader>
|
||||
<CardTitle className="text-sm flex items-center gap-2">
|
||||
<AlertTriangle className="h-4 w-4 text-amber-600" />
|
||||
Warnings ({preview.warnings.length})
|
||||
</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<ul className="space-y-2 text-sm">
|
||||
{preview.warnings.map((warning: string, idx: number) => (
|
||||
<li key={idx} className="flex items-start gap-2">
|
||||
<span className="text-amber-600">•</span>
|
||||
<span>{warning}</span>
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
</CardContent>
|
||||
</Card>
|
||||
)}
|
||||
|
||||
{preview.assignments && preview.assignments.length > 0 && (
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle className="text-sm">Assignment Summary</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<div className="space-y-2 text-sm">
|
||||
<div className="flex justify-between">
|
||||
<span className="text-muted-foreground">Total assignments:</span>
|
||||
<span className="font-medium">{preview.assignments.length}</span>
|
||||
</div>
|
||||
<div className="flex justify-between">
|
||||
<span className="text-muted-foreground">Unique projects:</span>
|
||||
<span className="font-medium">
|
||||
{new Set(preview.assignments.map((a: any) => a.projectId)).size}
|
||||
</span>
|
||||
</div>
|
||||
<div className="flex justify-between">
|
||||
<span className="text-muted-foreground">Unique jurors:</span>
|
||||
<span className="font-medium">
|
||||
{new Set(preview.assignments.map((a: any) => a.userId)).size}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
)}
|
||||
</div>
|
||||
) : (
|
||||
<p className="text-sm text-muted-foreground">No preview data available</p>
|
||||
)}
|
||||
</ScrollArea>
|
||||
|
||||
<SheetFooter className="mt-6">
|
||||
<Button variant="outline" onClick={() => onOpenChange(false)}>
|
||||
Cancel
|
||||
</Button>
|
||||
<Button
|
||||
onClick={handleExecute}
|
||||
disabled={isExecuting || !preview?.assignments || preview.assignments.length === 0}
|
||||
>
|
||||
{isExecuting ? 'Executing...' : 'Execute Assignments'}
|
||||
</Button>
|
||||
</SheetFooter>
|
||||
</SheetContent>
|
||||
</Sheet>
|
||||
)
|
||||
}
|
||||
Reference in New Issue
Block a user