Fix evaluation criteria, jury preferences, assignment config, and dashboard stats
All checks were successful
Build and Push Docker Image / build (push) Successful in 9m5s
All checks were successful
Build and Push Docker Image / build (push) Successful in 9m5s
- Fix criteria not showing for jurors: fetch active form independently via
getStageForm query instead of relying on existing evaluation record
- Fix scoringMode default from 'global' to 'criteria' (matching schema)
- Parse scale string format ("1-10") into minScore/maxScore for criteria display
- Fix COI dialog dismissal: prevent outside click on evaluate page Dialog
- Fix requiredReviews hardcoded to 3: read from round configJson in 4 locations
- Add jury preferences banner for unconfirmed caps on jury dashboard
- Add updateJuryPreferences tRPC procedure for self-service cap/ratio
- Simplify onboarding: always show jury step, allow cap up to 50
- Add role/ratio/availability fields to jury member invite dialog
- Simplify jury group settings (keep only defaultMaxAssignments)
- Enforce deliberation showCollectiveRankings flag for non-admin users
- Redesign dashboard stat cards: editorial data strip on mobile,
clean grid layout on desktop (no more generic card pattern)
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -1,13 +1,6 @@
|
||||
'use client'
|
||||
|
||||
import { Card, CardContent } from '@/components/ui/card'
|
||||
import { AnimatedCard } from '@/components/shared/animated-container'
|
||||
import {
|
||||
ClipboardList,
|
||||
CheckCircle2,
|
||||
FileEdit,
|
||||
Users,
|
||||
} from 'lucide-react'
|
||||
import { motion } from 'motion/react'
|
||||
|
||||
type PipelineRound = {
|
||||
id: string
|
||||
@@ -29,91 +22,73 @@ export function RoundStatsEvaluation({ round, activeJurors }: RoundStatsEvaluati
|
||||
const { assignmentCount, evalSubmitted, evalDraft, evalTotal } = round
|
||||
const completionPct = evalTotal > 0 ? ((evalSubmitted / evalTotal) * 100).toFixed(0) : '0'
|
||||
|
||||
const stats = [
|
||||
{
|
||||
value: assignmentCount,
|
||||
label: 'Assignments',
|
||||
detail: 'Jury-project pairs',
|
||||
accent: 'text-brand-blue',
|
||||
},
|
||||
{
|
||||
value: `${evalSubmitted}/${evalTotal}`,
|
||||
label: 'Submitted',
|
||||
detail: `${completionPct}% complete`,
|
||||
accent: 'text-emerald-600',
|
||||
},
|
||||
{
|
||||
value: evalDraft,
|
||||
label: 'In draft',
|
||||
detail: evalDraft > 0 ? 'Not yet submitted' : 'No drafts',
|
||||
accent: evalDraft > 0 ? 'text-amber-600' : 'text-emerald-600',
|
||||
},
|
||||
{
|
||||
value: activeJurors,
|
||||
label: 'Active jurors',
|
||||
detail: 'Evaluating',
|
||||
accent: 'text-brand-teal',
|
||||
},
|
||||
]
|
||||
|
||||
return (
|
||||
<div>
|
||||
<p className="mb-2 text-xs font-medium uppercase tracking-wider text-muted-foreground">
|
||||
{round.name} — Evaluation
|
||||
<>
|
||||
{/* Round label */}
|
||||
<p className="mb-2 text-[11px] font-semibold uppercase tracking-widest text-muted-foreground/70">
|
||||
{round.name} — Evaluation
|
||||
</p>
|
||||
<div className="grid gap-3 sm:grid-cols-2 lg:grid-cols-4">
|
||||
<AnimatedCard index={0}>
|
||||
<Card className="border-l-4 border-l-brand-blue transition-all duration-200 hover:-translate-y-0.5 hover:shadow-md">
|
||||
<CardContent className="p-4">
|
||||
<div className="flex items-center justify-between">
|
||||
<div className="min-w-0">
|
||||
<p className="text-xs font-semibold uppercase tracking-wider text-muted-foreground">Total Assignments</p>
|
||||
<p className="mt-1 text-2xl font-bold tabular-nums">{assignmentCount}</p>
|
||||
<p className="mt-0.5 text-xs text-brand-blue-light">
|
||||
Jury-project pairs
|
||||
</p>
|
||||
</div>
|
||||
<div className="flex h-10 w-10 shrink-0 items-center justify-center rounded-full bg-brand-blue/10">
|
||||
<ClipboardList className="h-5 w-5 text-brand-blue" />
|
||||
</div>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</AnimatedCard>
|
||||
|
||||
<AnimatedCard index={1}>
|
||||
<Card className="border-l-4 border-l-emerald-500 transition-all duration-200 hover:-translate-y-0.5 hover:shadow-md">
|
||||
<CardContent className="p-4">
|
||||
<div className="flex items-center justify-between">
|
||||
<div className="min-w-0">
|
||||
<p className="text-xs font-semibold uppercase tracking-wider text-muted-foreground">Submitted</p>
|
||||
<p className="mt-1 text-2xl font-bold tabular-nums">
|
||||
{evalSubmitted}
|
||||
<span className="text-sm font-normal text-muted-foreground">/{evalTotal}</span>
|
||||
</p>
|
||||
<p className="mt-0.5 text-xs text-emerald-600">
|
||||
{completionPct}% complete
|
||||
</p>
|
||||
</div>
|
||||
<div className="flex h-10 w-10 shrink-0 items-center justify-center rounded-full bg-emerald-500/10">
|
||||
<CheckCircle2 className="h-5 w-5 text-emerald-500" />
|
||||
</div>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</AnimatedCard>
|
||||
{/* Mobile: horizontal data strip */}
|
||||
<motion.div
|
||||
initial={{ opacity: 0 }}
|
||||
animate={{ opacity: 1 }}
|
||||
transition={{ duration: 0.4 }}
|
||||
className="flex items-baseline justify-between border-b border-t py-3 md:hidden"
|
||||
>
|
||||
{stats.map((s, i) => (
|
||||
<div key={i} className={`flex-1 text-center ${i > 0 ? 'border-l border-border/50' : ''}`}>
|
||||
<span className="text-xl font-bold tabular-nums tracking-tight">{s.value}</span>
|
||||
<p className="text-[10px] font-medium uppercase tracking-wider text-muted-foreground mt-0.5">{s.label}</p>
|
||||
</div>
|
||||
))}
|
||||
</motion.div>
|
||||
|
||||
<AnimatedCard index={2}>
|
||||
<Card className="border-l-4 border-l-amber-500 transition-all duration-200 hover:-translate-y-0.5 hover:shadow-md">
|
||||
<CardContent className="p-4">
|
||||
<div className="flex items-center justify-between">
|
||||
<div className="min-w-0">
|
||||
<p className="text-xs font-semibold uppercase tracking-wider text-muted-foreground">In Draft</p>
|
||||
<p className="mt-1 text-2xl font-bold tabular-nums">{evalDraft}</p>
|
||||
<p className="mt-0.5 text-xs text-amber-600">
|
||||
{evalDraft > 0 ? 'Not yet submitted' : 'No drafts'}
|
||||
</p>
|
||||
</div>
|
||||
<div className="flex h-10 w-10 shrink-0 items-center justify-center rounded-full bg-amber-500/10">
|
||||
<FileEdit className="h-5 w-5 text-amber-500" />
|
||||
</div>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</AnimatedCard>
|
||||
|
||||
<AnimatedCard index={3}>
|
||||
<Card className="border-l-4 border-l-violet-500 transition-all duration-200 hover:-translate-y-0.5 hover:shadow-md">
|
||||
<CardContent className="p-4">
|
||||
<div className="flex items-center justify-between">
|
||||
<div className="min-w-0">
|
||||
<p className="text-xs font-semibold uppercase tracking-wider text-muted-foreground">Active Jurors</p>
|
||||
<p className="mt-1 text-2xl font-bold tabular-nums">{activeJurors}</p>
|
||||
<p className="mt-0.5 text-xs text-violet-600">
|
||||
Evaluating this round
|
||||
</p>
|
||||
</div>
|
||||
<div className="flex h-10 w-10 shrink-0 items-center justify-center rounded-full bg-violet-500/10">
|
||||
<Users className="h-5 w-5 text-violet-500" />
|
||||
</div>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</AnimatedCard>
|
||||
{/* Desktop: editorial stat row */}
|
||||
<div className="hidden md:block">
|
||||
<div className="grid grid-cols-4 gap-px rounded-lg bg-border/40 overflow-hidden">
|
||||
{stats.map((s, i) => (
|
||||
<motion.div
|
||||
key={i}
|
||||
initial={{ opacity: 0, y: 8 }}
|
||||
animate={{ opacity: 1, y: 0 }}
|
||||
transition={{ duration: 0.3, delay: i * 0.06 }}
|
||||
className="bg-background px-5 py-4 group hover:bg-muted/30 transition-colors"
|
||||
>
|
||||
<span className="text-3xl font-bold tabular-nums tracking-tight">{s.value}</span>
|
||||
<p className="text-xs font-medium text-muted-foreground mt-1">{s.label}</p>
|
||||
<p className={`text-xs mt-0.5 ${s.accent}`}>{s.detail}</p>
|
||||
</motion.div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -1,13 +1,6 @@
|
||||
'use client'
|
||||
|
||||
import { Card, CardContent } from '@/components/ui/card'
|
||||
import { AnimatedCard } from '@/components/shared/animated-container'
|
||||
import {
|
||||
Filter,
|
||||
CheckCircle2,
|
||||
XCircle,
|
||||
AlertTriangle,
|
||||
} from 'lucide-react'
|
||||
import { motion } from 'motion/react'
|
||||
|
||||
type PipelineRound = {
|
||||
id: string
|
||||
@@ -35,93 +28,80 @@ type RoundStatsFilteringProps = {
|
||||
|
||||
export function RoundStatsFiltering({ round }: RoundStatsFilteringProps) {
|
||||
const { filteringPassed, filteringRejected, filteringFlagged, projectStates } = round
|
||||
const passRate = projectStates.total > 0
|
||||
? ((filteringPassed / projectStates.total) * 100).toFixed(0)
|
||||
: '0'
|
||||
const rejectRate = projectStates.total > 0
|
||||
? ((filteringRejected / projectStates.total) * 100).toFixed(0)
|
||||
: '0'
|
||||
|
||||
const stats = [
|
||||
{
|
||||
value: projectStates.total,
|
||||
label: 'To filter',
|
||||
detail: 'In pipeline',
|
||||
accent: 'text-brand-blue',
|
||||
},
|
||||
{
|
||||
value: filteringPassed,
|
||||
label: 'Passed',
|
||||
detail: `${passRate}% pass rate`,
|
||||
accent: 'text-emerald-600',
|
||||
},
|
||||
{
|
||||
value: filteringRejected,
|
||||
label: 'Rejected',
|
||||
detail: `${rejectRate}% rejected`,
|
||||
accent: 'text-red-600',
|
||||
},
|
||||
{
|
||||
value: filteringFlagged,
|
||||
label: 'Flagged',
|
||||
detail: filteringFlagged > 0 ? 'Manual review' : 'None flagged',
|
||||
accent: filteringFlagged > 0 ? 'text-amber-600' : 'text-emerald-600',
|
||||
},
|
||||
]
|
||||
|
||||
return (
|
||||
<div>
|
||||
<p className="mb-2 text-xs font-medium uppercase tracking-wider text-muted-foreground">
|
||||
{round.name} — Filtering
|
||||
<>
|
||||
{/* Round label */}
|
||||
<p className="mb-2 text-[11px] font-semibold uppercase tracking-widest text-muted-foreground/70">
|
||||
{round.name} — Filtering
|
||||
</p>
|
||||
<div className="grid gap-3 sm:grid-cols-2 lg:grid-cols-4">
|
||||
<AnimatedCard index={0}>
|
||||
<Card className="border-l-4 border-l-brand-blue transition-all duration-200 hover:-translate-y-0.5 hover:shadow-md">
|
||||
<CardContent className="p-4">
|
||||
<div className="flex items-center justify-between">
|
||||
<div className="min-w-0">
|
||||
<p className="text-xs font-semibold uppercase tracking-wider text-muted-foreground">Projects to Filter</p>
|
||||
<p className="mt-1 text-2xl font-bold tabular-nums">{projectStates.total}</p>
|
||||
<p className="mt-0.5 text-xs text-brand-blue-light">
|
||||
In pipeline
|
||||
</p>
|
||||
</div>
|
||||
<div className="flex h-10 w-10 shrink-0 items-center justify-center rounded-full bg-brand-blue/10">
|
||||
<Filter className="h-5 w-5 text-brand-blue" />
|
||||
</div>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</AnimatedCard>
|
||||
|
||||
<AnimatedCard index={1}>
|
||||
<Card className="border-l-4 border-l-emerald-500 transition-all duration-200 hover:-translate-y-0.5 hover:shadow-md">
|
||||
<CardContent className="p-4">
|
||||
<div className="flex items-center justify-between">
|
||||
<div className="min-w-0">
|
||||
<p className="text-xs font-semibold uppercase tracking-wider text-muted-foreground">AI Passed</p>
|
||||
<p className="mt-1 text-2xl font-bold tabular-nums">{filteringPassed}</p>
|
||||
<p className="mt-0.5 text-xs text-emerald-600">
|
||||
{projectStates.total > 0
|
||||
? `${((filteringPassed / projectStates.total) * 100).toFixed(0)}% pass rate`
|
||||
: 'No results yet'}
|
||||
</p>
|
||||
</div>
|
||||
<div className="flex h-10 w-10 shrink-0 items-center justify-center rounded-full bg-emerald-500/10">
|
||||
<CheckCircle2 className="h-5 w-5 text-emerald-500" />
|
||||
</div>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</AnimatedCard>
|
||||
{/* Mobile: horizontal data strip */}
|
||||
<motion.div
|
||||
initial={{ opacity: 0 }}
|
||||
animate={{ opacity: 1 }}
|
||||
transition={{ duration: 0.4 }}
|
||||
className="flex items-baseline justify-between border-b border-t py-3 md:hidden"
|
||||
>
|
||||
{stats.map((s, i) => (
|
||||
<div key={i} className={`flex-1 text-center ${i > 0 ? 'border-l border-border/50' : ''}`}>
|
||||
<span className="text-xl font-bold tabular-nums tracking-tight">{s.value}</span>
|
||||
<p className="text-[10px] font-medium uppercase tracking-wider text-muted-foreground mt-0.5">{s.label}</p>
|
||||
</div>
|
||||
))}
|
||||
</motion.div>
|
||||
|
||||
<AnimatedCard index={2}>
|
||||
<Card className="border-l-4 border-l-red-500 transition-all duration-200 hover:-translate-y-0.5 hover:shadow-md">
|
||||
<CardContent className="p-4">
|
||||
<div className="flex items-center justify-between">
|
||||
<div className="min-w-0">
|
||||
<p className="text-xs font-semibold uppercase tracking-wider text-muted-foreground">AI Rejected</p>
|
||||
<p className="mt-1 text-2xl font-bold tabular-nums">{filteringRejected}</p>
|
||||
<p className="mt-0.5 text-xs text-red-600">
|
||||
{projectStates.total > 0
|
||||
? `${((filteringRejected / projectStates.total) * 100).toFixed(0)}% rejected`
|
||||
: 'No results yet'}
|
||||
</p>
|
||||
</div>
|
||||
<div className="flex h-10 w-10 shrink-0 items-center justify-center rounded-full bg-red-500/10">
|
||||
<XCircle className="h-5 w-5 text-red-500" />
|
||||
</div>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</AnimatedCard>
|
||||
|
||||
<AnimatedCard index={3}>
|
||||
<Card className="border-l-4 border-l-amber-500 transition-all duration-200 hover:-translate-y-0.5 hover:shadow-md">
|
||||
<CardContent className="p-4">
|
||||
<div className="flex items-center justify-between">
|
||||
<div className="min-w-0">
|
||||
<p className="text-xs font-semibold uppercase tracking-wider text-muted-foreground">Flagged for Review</p>
|
||||
<p className="mt-1 text-2xl font-bold tabular-nums">{filteringFlagged}</p>
|
||||
<p className="mt-0.5 text-xs text-amber-600">
|
||||
{filteringFlagged > 0 ? 'Needs manual review' : 'None flagged'}
|
||||
</p>
|
||||
</div>
|
||||
<div className="flex h-10 w-10 shrink-0 items-center justify-center rounded-full bg-amber-500/10">
|
||||
<AlertTriangle className="h-5 w-5 text-amber-500" />
|
||||
</div>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</AnimatedCard>
|
||||
{/* Desktop: editorial stat row */}
|
||||
<div className="hidden md:block">
|
||||
<div className="grid grid-cols-4 gap-px rounded-lg bg-border/40 overflow-hidden">
|
||||
{stats.map((s, i) => (
|
||||
<motion.div
|
||||
key={i}
|
||||
initial={{ opacity: 0, y: 8 }}
|
||||
animate={{ opacity: 1, y: 0 }}
|
||||
transition={{ duration: 0.3, delay: i * 0.06 }}
|
||||
className="bg-background px-5 py-4 group hover:bg-muted/30 transition-colors"
|
||||
>
|
||||
<span className="text-3xl font-bold tabular-nums tracking-tight">{s.value}</span>
|
||||
<p className="text-xs font-medium text-muted-foreground mt-1">{s.label}</p>
|
||||
<p className={`text-xs mt-0.5 ${s.accent}`}>{s.detail}</p>
|
||||
</motion.div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -1,13 +1,6 @@
|
||||
'use client'
|
||||
|
||||
import { Card, CardContent } from '@/components/ui/card'
|
||||
import { AnimatedCard } from '@/components/shared/animated-container'
|
||||
import {
|
||||
ClipboardList,
|
||||
Users,
|
||||
CheckCircle2,
|
||||
AlertTriangle,
|
||||
} from 'lucide-react'
|
||||
import { motion } from 'motion/react'
|
||||
|
||||
type RoundStatsGenericProps = {
|
||||
projectCount: number
|
||||
@@ -33,89 +26,72 @@ export function RoundStatsGeneric({
|
||||
const completionPct =
|
||||
totalAssignments > 0 ? ((submittedCount / totalAssignments) * 100).toFixed(0) : '0'
|
||||
|
||||
const stats = [
|
||||
{
|
||||
value: projectCount,
|
||||
label: 'Projects',
|
||||
detail: newProjectsThisWeek > 0 ? `+${newProjectsThisWeek} this week` : null,
|
||||
accent: 'text-brand-blue',
|
||||
},
|
||||
{
|
||||
value: totalJurors,
|
||||
label: 'Jurors',
|
||||
detail: `${activeJurors} active`,
|
||||
accent: 'text-brand-teal',
|
||||
},
|
||||
{
|
||||
value: `${submittedCount}/${totalAssignments}`,
|
||||
label: 'Evaluations',
|
||||
detail: `${completionPct}% complete`,
|
||||
accent: 'text-emerald-600',
|
||||
},
|
||||
{
|
||||
value: actionsCount,
|
||||
label: actionsCount === 1 ? 'Action' : 'Actions',
|
||||
detail: actionsCount > 0 ? 'Pending' : 'All clear',
|
||||
accent: actionsCount > 0 ? 'text-amber-600' : 'text-emerald-600',
|
||||
},
|
||||
]
|
||||
|
||||
return (
|
||||
<div className="grid gap-3 sm:grid-cols-2 lg:grid-cols-4">
|
||||
<AnimatedCard index={0}>
|
||||
<Card className="border-l-4 border-l-brand-blue transition-all duration-200 hover:-translate-y-0.5 hover:shadow-md">
|
||||
<CardContent className="p-4">
|
||||
<div className="flex items-center justify-between">
|
||||
<div className="min-w-0">
|
||||
<p className="text-xs font-semibold uppercase tracking-wider text-muted-foreground">Projects</p>
|
||||
<p className="mt-1 text-2xl font-bold tabular-nums">{projectCount}</p>
|
||||
<p className="mt-0.5 text-xs text-brand-blue-light">
|
||||
{newProjectsThisWeek > 0 ? `+${newProjectsThisWeek} this week` : 'In edition'}
|
||||
</p>
|
||||
</div>
|
||||
<div className="flex h-10 w-10 shrink-0 items-center justify-center rounded-full bg-brand-blue/10">
|
||||
<ClipboardList className="h-5 w-5 text-brand-blue" />
|
||||
</div>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</AnimatedCard>
|
||||
<>
|
||||
{/* Mobile: horizontal data strip */}
|
||||
<motion.div
|
||||
initial={{ opacity: 0 }}
|
||||
animate={{ opacity: 1 }}
|
||||
transition={{ duration: 0.4 }}
|
||||
className="flex items-baseline justify-between border-b border-t py-3 md:hidden"
|
||||
>
|
||||
{stats.map((s, i) => (
|
||||
<div key={i} className={`flex-1 text-center ${i > 0 ? 'border-l border-border/50' : ''}`}>
|
||||
<span className="text-xl font-bold tabular-nums tracking-tight">{s.value}</span>
|
||||
<p className="text-[10px] font-medium uppercase tracking-wider text-muted-foreground mt-0.5">{s.label}</p>
|
||||
</div>
|
||||
))}
|
||||
</motion.div>
|
||||
|
||||
<AnimatedCard index={1}>
|
||||
<Card className="border-l-4 border-l-violet-500 transition-all duration-200 hover:-translate-y-0.5 hover:shadow-md">
|
||||
<CardContent className="p-4">
|
||||
<div className="flex items-center justify-between">
|
||||
<div className="min-w-0">
|
||||
<p className="text-xs font-semibold uppercase tracking-wider text-muted-foreground">Jury</p>
|
||||
<p className="mt-1 text-2xl font-bold tabular-nums">{totalJurors}</p>
|
||||
<p className="mt-0.5 text-xs text-violet-600">
|
||||
{activeJurors} active
|
||||
</p>
|
||||
{/* Desktop: editorial stat row */}
|
||||
<div className="hidden md:block">
|
||||
<div className="grid grid-cols-4 gap-px rounded-lg bg-border/40 overflow-hidden">
|
||||
{stats.map((s, i) => (
|
||||
<motion.div
|
||||
key={i}
|
||||
initial={{ opacity: 0, y: 8 }}
|
||||
animate={{ opacity: 1, y: 0 }}
|
||||
transition={{ duration: 0.3, delay: i * 0.06 }}
|
||||
className="bg-background px-5 py-4 group hover:bg-muted/30 transition-colors"
|
||||
>
|
||||
<div className="flex items-baseline gap-2">
|
||||
<span className="text-3xl font-bold tabular-nums tracking-tight">{s.value}</span>
|
||||
</div>
|
||||
<div className="flex h-10 w-10 shrink-0 items-center justify-center rounded-full bg-violet-500/10">
|
||||
<Users className="h-5 w-5 text-violet-500" />
|
||||
</div>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</AnimatedCard>
|
||||
|
||||
<AnimatedCard index={2}>
|
||||
<Card className="border-l-4 border-l-emerald-500 transition-all duration-200 hover:-translate-y-0.5 hover:shadow-md">
|
||||
<CardContent className="p-4">
|
||||
<div className="flex items-center justify-between">
|
||||
<div className="min-w-0">
|
||||
<p className="text-xs font-semibold uppercase tracking-wider text-muted-foreground">Evaluations</p>
|
||||
<p className="mt-1 text-2xl font-bold tabular-nums">
|
||||
{submittedCount}
|
||||
<span className="text-sm font-normal text-muted-foreground">/{totalAssignments}</span>
|
||||
</p>
|
||||
<p className="mt-0.5 text-xs text-emerald-600">
|
||||
{completionPct}% complete
|
||||
</p>
|
||||
</div>
|
||||
<div className="flex h-10 w-10 shrink-0 items-center justify-center rounded-full bg-emerald-500/10">
|
||||
<CheckCircle2 className="h-5 w-5 text-emerald-500" />
|
||||
</div>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</AnimatedCard>
|
||||
|
||||
<AnimatedCard index={3}>
|
||||
<Card className={`border-l-4 transition-all duration-200 hover:-translate-y-0.5 hover:shadow-md ${actionsCount > 0 ? 'border-l-amber-500' : 'border-l-emerald-400'}`}>
|
||||
<CardContent className="p-4">
|
||||
<div className="flex items-center justify-between">
|
||||
<div className="min-w-0">
|
||||
<p className="text-xs font-semibold uppercase tracking-wider text-muted-foreground">Actions Needed</p>
|
||||
<p className="mt-1 text-2xl font-bold tabular-nums">{actionsCount}</p>
|
||||
<p className={`mt-0.5 text-xs ${actionsCount > 0 ? 'text-amber-600' : 'text-emerald-600'}`}>
|
||||
{actionsCount > 0 ? 'Pending actions' : 'All clear'}
|
||||
</p>
|
||||
</div>
|
||||
<div className={`flex h-10 w-10 shrink-0 items-center justify-center rounded-full ${actionsCount > 0 ? 'bg-amber-500/10' : 'bg-emerald-400/10'}`}>
|
||||
{actionsCount > 0
|
||||
? <AlertTriangle className="h-5 w-5 text-amber-500" />
|
||||
: <CheckCircle2 className="h-5 w-5 text-emerald-400" />
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</AnimatedCard>
|
||||
</div>
|
||||
<p className="text-xs font-medium text-muted-foreground mt-1">{s.label}</p>
|
||||
{s.detail && (
|
||||
<p className={`text-xs mt-0.5 ${s.accent}`}>{s.detail}</p>
|
||||
)}
|
||||
</motion.div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -1,13 +1,6 @@
|
||||
'use client'
|
||||
|
||||
import { Card, CardContent } from '@/components/ui/card'
|
||||
import { AnimatedCard } from '@/components/shared/animated-container'
|
||||
import {
|
||||
ClipboardList,
|
||||
CheckCircle2,
|
||||
Clock,
|
||||
TrendingUp,
|
||||
} from 'lucide-react'
|
||||
import { motion } from 'motion/react'
|
||||
|
||||
type PipelineRound = {
|
||||
id: string
|
||||
@@ -32,91 +25,77 @@ type RoundStatsIntakeProps = {
|
||||
|
||||
export function RoundStatsIntake({ round, newProjectsThisWeek }: RoundStatsIntakeProps) {
|
||||
const { projectStates } = round
|
||||
const completePct = projectStates.total > 0
|
||||
? ((projectStates.PASSED / projectStates.total) * 100).toFixed(0)
|
||||
: '0'
|
||||
|
||||
const stats = [
|
||||
{
|
||||
value: projectStates.total,
|
||||
label: 'Submitted',
|
||||
detail: 'Total projects',
|
||||
accent: 'text-brand-blue',
|
||||
},
|
||||
{
|
||||
value: projectStates.PASSED,
|
||||
label: 'Docs complete',
|
||||
detail: `${completePct}% of total`,
|
||||
accent: 'text-emerald-600',
|
||||
},
|
||||
{
|
||||
value: projectStates.PENDING,
|
||||
label: 'Pending',
|
||||
detail: projectStates.PENDING > 0 ? 'Awaiting review' : 'All reviewed',
|
||||
accent: projectStates.PENDING > 0 ? 'text-amber-600' : 'text-emerald-600',
|
||||
},
|
||||
{
|
||||
value: newProjectsThisWeek,
|
||||
label: 'This week',
|
||||
detail: newProjectsThisWeek > 0 ? 'New submissions' : 'No new',
|
||||
accent: 'text-brand-teal',
|
||||
},
|
||||
]
|
||||
|
||||
return (
|
||||
<div>
|
||||
<p className="mb-2 text-xs font-medium uppercase tracking-wider text-muted-foreground">
|
||||
{round.name} — Intake
|
||||
<>
|
||||
{/* Round label */}
|
||||
<p className="mb-2 text-[11px] font-semibold uppercase tracking-widest text-muted-foreground/70">
|
||||
{round.name} — Intake
|
||||
</p>
|
||||
<div className="grid gap-3 sm:grid-cols-2 lg:grid-cols-4">
|
||||
<AnimatedCard index={0}>
|
||||
<Card className="border-l-4 border-l-brand-blue transition-all duration-200 hover:-translate-y-0.5 hover:shadow-md">
|
||||
<CardContent className="p-4">
|
||||
<div className="flex items-center justify-between">
|
||||
<div className="min-w-0">
|
||||
<p className="text-xs font-semibold uppercase tracking-wider text-muted-foreground">Total Projects</p>
|
||||
<p className="mt-1 text-2xl font-bold tabular-nums">{projectStates.total}</p>
|
||||
<p className="mt-0.5 text-xs text-brand-blue-light">
|
||||
In this round
|
||||
</p>
|
||||
</div>
|
||||
<div className="flex h-10 w-10 shrink-0 items-center justify-center rounded-full bg-brand-blue/10">
|
||||
<ClipboardList className="h-5 w-5 text-brand-blue" />
|
||||
</div>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</AnimatedCard>
|
||||
|
||||
<AnimatedCard index={1}>
|
||||
<Card className="border-l-4 border-l-emerald-500 transition-all duration-200 hover:-translate-y-0.5 hover:shadow-md">
|
||||
<CardContent className="p-4">
|
||||
<div className="flex items-center justify-between">
|
||||
<div className="min-w-0">
|
||||
<p className="text-xs font-semibold uppercase tracking-wider text-muted-foreground">Documents Complete</p>
|
||||
<p className="mt-1 text-2xl font-bold tabular-nums">{projectStates.PASSED}</p>
|
||||
<p className="mt-0.5 text-xs text-emerald-600">
|
||||
{projectStates.total > 0
|
||||
? `${((projectStates.PASSED / projectStates.total) * 100).toFixed(0)}% of total`
|
||||
: 'No projects yet'}
|
||||
</p>
|
||||
</div>
|
||||
<div className="flex h-10 w-10 shrink-0 items-center justify-center rounded-full bg-emerald-500/10">
|
||||
<CheckCircle2 className="h-5 w-5 text-emerald-500" />
|
||||
</div>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</AnimatedCard>
|
||||
{/* Mobile: horizontal data strip */}
|
||||
<motion.div
|
||||
initial={{ opacity: 0 }}
|
||||
animate={{ opacity: 1 }}
|
||||
transition={{ duration: 0.4 }}
|
||||
className="flex items-baseline justify-between border-b border-t py-3 md:hidden"
|
||||
>
|
||||
{stats.map((s, i) => (
|
||||
<div key={i} className={`flex-1 text-center ${i > 0 ? 'border-l border-border/50' : ''}`}>
|
||||
<span className="text-xl font-bold tabular-nums tracking-tight">{s.value}</span>
|
||||
<p className="text-[10px] font-medium uppercase tracking-wider text-muted-foreground mt-0.5">{s.label}</p>
|
||||
</div>
|
||||
))}
|
||||
</motion.div>
|
||||
|
||||
<AnimatedCard index={2}>
|
||||
<Card className="border-l-4 border-l-amber-500 transition-all duration-200 hover:-translate-y-0.5 hover:shadow-md">
|
||||
<CardContent className="p-4">
|
||||
<div className="flex items-center justify-between">
|
||||
<div className="min-w-0">
|
||||
<p className="text-xs font-semibold uppercase tracking-wider text-muted-foreground">Pending Review</p>
|
||||
<p className="mt-1 text-2xl font-bold tabular-nums">{projectStates.PENDING}</p>
|
||||
<p className="mt-0.5 text-xs text-amber-600">
|
||||
{projectStates.PENDING > 0 ? 'Awaiting review' : 'All reviewed'}
|
||||
</p>
|
||||
</div>
|
||||
<div className="flex h-10 w-10 shrink-0 items-center justify-center rounded-full bg-amber-500/10">
|
||||
<Clock className="h-5 w-5 text-amber-500" />
|
||||
</div>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</AnimatedCard>
|
||||
|
||||
<AnimatedCard index={3}>
|
||||
<Card className="border-l-4 border-l-brand-teal transition-all duration-200 hover:-translate-y-0.5 hover:shadow-md">
|
||||
<CardContent className="p-4">
|
||||
<div className="flex items-center justify-between">
|
||||
<div className="min-w-0">
|
||||
<p className="text-xs font-semibold uppercase tracking-wider text-muted-foreground">New This Week</p>
|
||||
<p className="mt-1 text-2xl font-bold tabular-nums">{newProjectsThisWeek}</p>
|
||||
<p className="mt-0.5 text-xs text-brand-teal">
|
||||
{newProjectsThisWeek > 0 ? 'Recently submitted' : 'No new submissions'}
|
||||
</p>
|
||||
</div>
|
||||
<div className="flex h-10 w-10 shrink-0 items-center justify-center rounded-full bg-brand-teal/10">
|
||||
<TrendingUp className="h-5 w-5 text-brand-teal" />
|
||||
</div>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</AnimatedCard>
|
||||
{/* Desktop: editorial stat row */}
|
||||
<div className="hidden md:block">
|
||||
<div className="grid grid-cols-4 gap-px rounded-lg bg-border/40 overflow-hidden">
|
||||
{stats.map((s, i) => (
|
||||
<motion.div
|
||||
key={i}
|
||||
initial={{ opacity: 0, y: 8 }}
|
||||
animate={{ opacity: 1, y: 0 }}
|
||||
transition={{ duration: 0.3, delay: i * 0.06 }}
|
||||
className="bg-background px-5 py-4 group hover:bg-muted/30 transition-colors"
|
||||
>
|
||||
<span className="text-3xl font-bold tabular-nums tracking-tight">{s.value}</span>
|
||||
<p className="text-xs font-medium text-muted-foreground mt-1">{s.label}</p>
|
||||
<p className={`text-xs mt-0.5 ${s.accent}`}>{s.detail}</p>
|
||||
</motion.div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user