Competition/Round architecture: full platform rewrite (Phases 1-9)
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>
2026-02-15 23:04:15 +01:00
|
|
|
'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 })
|
Platform-wide UX fixes: assignment dialog, invalidation, settings, dashboard
1. Assignment dialog overhaul: replace raw UUID inputs with searchable
juror Combobox (shows name, email, capacity) and multi-select project
checklist with bulk assignment support
2. Query invalidation sweep: fix missing invalidations in
assignment-preview-sheet (roundAssignment.execute) and
filtering-dashboard (filtering.finalizeResults) so data refreshes
without page reload
3. Rename Submissions tab to Document Windows with descriptive
header explaining upload window configuration
4. Connect 6 disconnected settings: storage_provider, local_storage_path,
avatar_max_size_mb, allowed_image_types, whatsapp_enabled,
whatsapp_provider - all now accessible in Settings UI
5. Admin dashboard redesign: branded Editorial Command Center with
Dark Blue gradient header, colored border-l-4 stat cards, staggered
animations, 2-column layout, action-required panel, activity timeline
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-16 16:05:25 +01:00
|
|
|
utils.assignment.listByStage.invalidate({ roundId })
|
|
|
|
|
utils.roundEngine.getProjectStates.invalidate({ roundId })
|
Competition/Round architecture: full platform rewrite (Phases 1-9)
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>
2026-02-15 23:04:15 +01:00
|
|
|
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>
|
|
|
|
|
)
|
|
|
|
|
}
|