Observer dashboard extraction, PDF reports, jury UX overhaul, and miscellaneous improvements
- Extract observer dashboard to client component, add PDF export button - Add PDF report generator with jsPDF for analytics reports - Overhaul jury evaluation page with improved layout and UX - Add new analytics endpoints for observer/admin reports - Improve round creation/edit forms with better settings - Fix filtering rules page, CSV export dialog, notification bell - Update auth, prisma schema, and various type fixes Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -72,7 +72,7 @@ interface PageProps {
|
||||
const updateRoundSchema = z
|
||||
.object({
|
||||
name: z.string().min(1, 'Name is required').max(255),
|
||||
requiredReviews: z.number().int().min(1).max(10),
|
||||
requiredReviews: z.number().int().min(0).max(10),
|
||||
minAssignmentsPerJuror: z.number().int().min(1).max(50),
|
||||
maxAssignmentsPerJuror: z.number().int().min(1).max(100),
|
||||
votingStartAt: z.date().nullable().optional(),
|
||||
@@ -206,7 +206,7 @@ function EditRoundContent({ roundId }: { roundId: string }) {
|
||||
await updateRound.mutateAsync({
|
||||
id: roundId,
|
||||
name: data.name,
|
||||
requiredReviews: data.requiredReviews,
|
||||
requiredReviews: roundType === 'FILTERING' ? 0 : data.requiredReviews,
|
||||
minAssignmentsPerJuror: data.minAssignmentsPerJuror,
|
||||
maxAssignmentsPerJuror: data.maxAssignmentsPerJuror,
|
||||
roundType,
|
||||
@@ -301,30 +301,32 @@ function EditRoundContent({ roundId }: { roundId: string }) {
|
||||
)}
|
||||
/>
|
||||
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="requiredReviews"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel>Required Reviews per Project</FormLabel>
|
||||
<FormControl>
|
||||
<Input
|
||||
type="number"
|
||||
min={1}
|
||||
max={10}
|
||||
{...field}
|
||||
onChange={(e) =>
|
||||
field.onChange(parseInt(e.target.value) || 1)
|
||||
}
|
||||
/>
|
||||
</FormControl>
|
||||
<FormDescription>
|
||||
Minimum number of evaluations each project should receive
|
||||
</FormDescription>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
{roundType !== 'FILTERING' && (
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="requiredReviews"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel>Required Reviews per Project</FormLabel>
|
||||
<FormControl>
|
||||
<Input
|
||||
type="number"
|
||||
min={1}
|
||||
max={10}
|
||||
{...field}
|
||||
onChange={(e) =>
|
||||
field.onChange(parseInt(e.target.value) || 1)
|
||||
}
|
||||
/>
|
||||
</FormControl>
|
||||
<FormDescription>
|
||||
Minimum number of evaluations each project should receive
|
||||
</FormDescription>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
)}
|
||||
|
||||
<div className="grid gap-4 sm:grid-cols-2">
|
||||
<FormField
|
||||
|
||||
@@ -105,6 +105,8 @@ export default function FilteringResultsPage({
|
||||
: undefined,
|
||||
page,
|
||||
perPage,
|
||||
}, {
|
||||
staleTime: 0, // Always refetch - results change after filtering runs
|
||||
})
|
||||
|
||||
const utils = trpc.useUtils()
|
||||
|
||||
@@ -109,6 +109,7 @@ export default function FilteringRulesPage({
|
||||
|
||||
// AI screening config state
|
||||
const [criteriaText, setCriteriaText] = useState('')
|
||||
const [aiAction, setAiAction] = useState<'REJECT' | 'FLAG'>('REJECT')
|
||||
const [aiBatchSize, setAiBatchSize] = useState('20')
|
||||
const [aiParallelBatches, setAiParallelBatches] = useState('1')
|
||||
|
||||
@@ -144,7 +145,7 @@ export default function FilteringRulesPage({
|
||||
} else if (newRuleType === 'AI_SCREENING') {
|
||||
configJson = {
|
||||
criteriaText,
|
||||
action: 'FLAG',
|
||||
action: aiAction,
|
||||
batchSize: parseInt(aiBatchSize) || 20,
|
||||
parallelBatches: parseInt(aiParallelBatches) || 1,
|
||||
}
|
||||
@@ -418,9 +419,23 @@ export default function FilteringRulesPage({
|
||||
placeholder="Describe the criteria for AI to evaluate projects against..."
|
||||
rows={4}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="space-y-2">
|
||||
<Label>Action for Non-Matching Projects</Label>
|
||||
<Select value={aiAction} onValueChange={(v) => setAiAction(v as 'REJECT' | 'FLAG')}>
|
||||
<SelectTrigger>
|
||||
<SelectValue />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectItem value="REJECT">Auto Filter Out</SelectItem>
|
||||
<SelectItem value="FLAG">Flag for Review</SelectItem>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
<p className="text-xs text-muted-foreground">
|
||||
AI screening always flags projects for human review, never
|
||||
auto-rejects.
|
||||
{aiAction === 'REJECT'
|
||||
? 'Projects that don\'t meet criteria will be automatically filtered out.'
|
||||
: 'Projects that don\'t meet criteria will be flagged for human review.'}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
|
||||
@@ -50,7 +50,7 @@ const TEAM_NOTIFICATION_OPTIONS = [
|
||||
const createRoundSchema = z.object({
|
||||
programId: z.string().min(1, 'Please select a program'),
|
||||
name: z.string().min(1, 'Name is required').max(255),
|
||||
requiredReviews: z.number().int().min(1).max(10),
|
||||
requiredReviews: z.number().int().min(0).max(10),
|
||||
votingStartAt: z.date().nullable().optional(),
|
||||
votingEndAt: z.date().nullable().optional(),
|
||||
}).refine((data) => {
|
||||
@@ -128,7 +128,7 @@ function CreateRoundContent() {
|
||||
programId: data.programId,
|
||||
name: data.name,
|
||||
roundType,
|
||||
requiredReviews: data.requiredReviews,
|
||||
requiredReviews: roundType === 'FILTERING' ? 0 : data.requiredReviews,
|
||||
settingsJson: roundSettings,
|
||||
votingStartAt: data.votingStartAt ?? undefined,
|
||||
votingEndAt: data.votingEndAt ?? undefined,
|
||||
@@ -291,28 +291,30 @@ function CreateRoundContent() {
|
||||
)}
|
||||
/>
|
||||
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="requiredReviews"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel>Required Reviews per Project</FormLabel>
|
||||
<FormControl>
|
||||
<Input
|
||||
type="number"
|
||||
min={1}
|
||||
max={10}
|
||||
{...field}
|
||||
onChange={(e) => field.onChange(parseInt(e.target.value) || 1)}
|
||||
/>
|
||||
</FormControl>
|
||||
<FormDescription>
|
||||
Minimum number of evaluations each project should receive
|
||||
</FormDescription>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
{roundType !== 'FILTERING' && (
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="requiredReviews"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel>Required Reviews per Project</FormLabel>
|
||||
<FormControl>
|
||||
<Input
|
||||
type="number"
|
||||
min={1}
|
||||
max={10}
|
||||
{...field}
|
||||
onChange={(e) => field.onChange(parseInt(e.target.value) || 1)}
|
||||
/>
|
||||
</FormControl>
|
||||
<FormDescription>
|
||||
Minimum number of evaluations each project should receive
|
||||
</FormDescription>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
)}
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
|
||||
Reference in New Issue
Block a user