Apply full refactor updates plus pipeline/email UX confirmations
All checks were successful
Build and Push Docker Image / build (push) Successful in 10m33s

This commit is contained in:
Matt
2026-02-14 15:26:42 +01:00
parent e56e143a40
commit b5425e705e
374 changed files with 116737 additions and 111969 deletions

View File

@@ -1,153 +1,153 @@
'use client'
import {
BarChart,
Bar,
XAxis,
YAxis,
CartesianGrid,
Tooltip,
ResponsiveContainer,
Legend,
} from 'recharts'
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card'
interface StageComparison {
stageId: string
stageName: string
projectCount: number
evaluationCount: number
completionRate: number
averageScore: number | null
scoreDistribution: { score: number; count: number }[]
}
interface CrossStageComparisonProps {
data: StageComparison[]
}
const STAGE_COLORS = ['#053d57', '#de0f1e', '#557f8c', '#f38a52', '#6ad82f']
export function CrossStageComparisonChart({ data }: CrossStageComparisonProps) {
// Prepare comparison data
const comparisonData = data.map((stage, i) => ({
name: stage.stageName.length > 20 ? stage.stageName.slice(0, 20) + '...' : stage.stageName,
projects: stage.projectCount,
evaluations: stage.evaluationCount,
completionRate: stage.completionRate,
avgScore: stage.averageScore ? parseFloat(stage.averageScore.toFixed(2)) : 0,
color: STAGE_COLORS[i % STAGE_COLORS.length],
}))
return (
<div className="space-y-6">
{/* Metrics Comparison */}
<Card>
<CardHeader>
<CardTitle>Stage Metrics Comparison</CardTitle>
</CardHeader>
<CardContent>
<div className="h-[350px]">
<ResponsiveContainer width="100%" height="100%">
<BarChart
data={comparisonData}
margin={{ top: 20, right: 30, bottom: 60, left: 20 }}
>
<CartesianGrid strokeDasharray="3 3" className="stroke-muted" />
<XAxis
dataKey="name"
angle={-25}
textAnchor="end"
height={60}
tick={{ fontSize: 12 }}
/>
<YAxis />
<Tooltip
contentStyle={{
backgroundColor: 'hsl(var(--card))',
border: '1px solid hsl(var(--border))',
borderRadius: '6px',
}}
/>
<Legend />
<Bar dataKey="projects" name="Projects" fill="#053d57" radius={[4, 4, 0, 0]} />
<Bar dataKey="evaluations" name="Evaluations" fill="#557f8c" radius={[4, 4, 0, 0]} />
</BarChart>
</ResponsiveContainer>
</div>
</CardContent>
</Card>
{/* Completion & Score Comparison */}
<div className="grid gap-6 lg:grid-cols-2">
<Card>
<CardHeader>
<CardTitle>Completion Rate by Stage</CardTitle>
</CardHeader>
<CardContent>
<div className="h-[300px]">
<ResponsiveContainer width="100%" height="100%">
<BarChart
data={comparisonData}
margin={{ top: 20, right: 20, bottom: 60, left: 20 }}
>
<CartesianGrid strokeDasharray="3 3" className="stroke-muted" />
<XAxis
dataKey="name"
angle={-25}
textAnchor="end"
height={60}
tick={{ fontSize: 12 }}
/>
<YAxis domain={[0, 100]} unit="%" />
<Tooltip
contentStyle={{
backgroundColor: 'hsl(var(--card))',
border: '1px solid hsl(var(--border))',
borderRadius: '6px',
}}
/>
<Bar dataKey="completionRate" name="Completion %" fill="#6ad82f" radius={[4, 4, 0, 0]} />
</BarChart>
</ResponsiveContainer>
</div>
</CardContent>
</Card>
<Card>
<CardHeader>
<CardTitle>Average Score by Stage</CardTitle>
</CardHeader>
<CardContent>
<div className="h-[300px]">
<ResponsiveContainer width="100%" height="100%">
<BarChart
data={comparisonData}
margin={{ top: 20, right: 20, bottom: 60, left: 20 }}
>
<CartesianGrid strokeDasharray="3 3" className="stroke-muted" />
<XAxis
dataKey="name"
angle={-25}
textAnchor="end"
height={60}
tick={{ fontSize: 12 }}
/>
<YAxis domain={[0, 10]} />
<Tooltip
contentStyle={{
backgroundColor: 'hsl(var(--card))',
border: '1px solid hsl(var(--border))',
borderRadius: '6px',
}}
/>
<Bar dataKey="avgScore" name="Avg Score" fill="#de0f1e" radius={[4, 4, 0, 0]} />
</BarChart>
</ResponsiveContainer>
</div>
</CardContent>
</Card>
</div>
</div>
)
}
'use client'
import {
BarChart,
Bar,
XAxis,
YAxis,
CartesianGrid,
Tooltip,
ResponsiveContainer,
Legend,
} from 'recharts'
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card'
interface StageComparison {
stageId: string
stageName: string
projectCount: number
evaluationCount: number
completionRate: number
averageScore: number | null
scoreDistribution: { score: number; count: number }[]
}
interface CrossStageComparisonProps {
data: StageComparison[]
}
const STAGE_COLORS = ['#053d57', '#de0f1e', '#557f8c', '#f38a52', '#6ad82f']
export function CrossStageComparisonChart({ data }: CrossStageComparisonProps) {
// Prepare comparison data
const comparisonData = data.map((stage, i) => ({
name: stage.stageName.length > 20 ? stage.stageName.slice(0, 20) + '...' : stage.stageName,
projects: stage.projectCount,
evaluations: stage.evaluationCount,
completionRate: stage.completionRate,
avgScore: stage.averageScore ? parseFloat(stage.averageScore.toFixed(2)) : 0,
color: STAGE_COLORS[i % STAGE_COLORS.length],
}))
return (
<div className="space-y-6">
{/* Metrics Comparison */}
<Card>
<CardHeader>
<CardTitle>Stage Metrics Comparison</CardTitle>
</CardHeader>
<CardContent>
<div className="h-[350px]">
<ResponsiveContainer width="100%" height="100%">
<BarChart
data={comparisonData}
margin={{ top: 20, right: 30, bottom: 60, left: 20 }}
>
<CartesianGrid strokeDasharray="3 3" className="stroke-muted" />
<XAxis
dataKey="name"
angle={-25}
textAnchor="end"
height={60}
tick={{ fontSize: 12 }}
/>
<YAxis />
<Tooltip
contentStyle={{
backgroundColor: 'hsl(var(--card))',
border: '1px solid hsl(var(--border))',
borderRadius: '6px',
}}
/>
<Legend />
<Bar dataKey="projects" name="Projects" fill="#053d57" radius={[4, 4, 0, 0]} />
<Bar dataKey="evaluations" name="Evaluations" fill="#557f8c" radius={[4, 4, 0, 0]} />
</BarChart>
</ResponsiveContainer>
</div>
</CardContent>
</Card>
{/* Completion & Score Comparison */}
<div className="grid gap-6 lg:grid-cols-2">
<Card>
<CardHeader>
<CardTitle>Completion Rate by Stage</CardTitle>
</CardHeader>
<CardContent>
<div className="h-[300px]">
<ResponsiveContainer width="100%" height="100%">
<BarChart
data={comparisonData}
margin={{ top: 20, right: 20, bottom: 60, left: 20 }}
>
<CartesianGrid strokeDasharray="3 3" className="stroke-muted" />
<XAxis
dataKey="name"
angle={-25}
textAnchor="end"
height={60}
tick={{ fontSize: 12 }}
/>
<YAxis domain={[0, 100]} unit="%" />
<Tooltip
contentStyle={{
backgroundColor: 'hsl(var(--card))',
border: '1px solid hsl(var(--border))',
borderRadius: '6px',
}}
/>
<Bar dataKey="completionRate" name="Completion %" fill="#6ad82f" radius={[4, 4, 0, 0]} />
</BarChart>
</ResponsiveContainer>
</div>
</CardContent>
</Card>
<Card>
<CardHeader>
<CardTitle>Average Score by Stage</CardTitle>
</CardHeader>
<CardContent>
<div className="h-[300px]">
<ResponsiveContainer width="100%" height="100%">
<BarChart
data={comparisonData}
margin={{ top: 20, right: 20, bottom: 60, left: 20 }}
>
<CartesianGrid strokeDasharray="3 3" className="stroke-muted" />
<XAxis
dataKey="name"
angle={-25}
textAnchor="end"
height={60}
tick={{ fontSize: 12 }}
/>
<YAxis domain={[0, 10]} />
<Tooltip
contentStyle={{
backgroundColor: 'hsl(var(--card))',
border: '1px solid hsl(var(--border))',
borderRadius: '6px',
}}
/>
<Bar dataKey="avgScore" name="Avg Score" fill="#de0f1e" radius={[4, 4, 0, 0]} />
</BarChart>
</ResponsiveContainer>
</div>
</CardContent>
</Card>
</div>
</div>
)
}

View File

@@ -1,279 +1,279 @@
'use client'
import {
PieChart,
Pie,
Cell,
Tooltip,
ResponsiveContainer,
Legend,
BarChart,
Bar,
XAxis,
YAxis,
CartesianGrid,
} from 'recharts'
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card'
import { Badge } from '@/components/ui/badge'
interface DiversityData {
total: number
byCountry: { country: string; count: number; percentage: number }[]
byCategory: { category: string; count: number; percentage: number }[]
byOceanIssue: { issue: string; count: number; percentage: number }[]
byTag: { tag: string; count: number; percentage: number }[]
}
interface DiversityMetricsProps {
data: DiversityData
}
const PIE_COLORS = [
'#053d57', '#de0f1e', '#557f8c', '#f38a52', '#6ad82f',
'#3be31e', '#c9c052', '#e6382f', '#ed6141', '#0bd90f',
'#8884d8', '#82ca9d', '#ffc658', '#ff7c7c', '#8dd1e1',
]
/** Convert ISO 3166-1 alpha-2 code to full country name using Intl API */
function getCountryName(code: string): string {
if (code === 'Others') return 'Others'
try {
const displayNames = new Intl.DisplayNames(['en'], { type: 'region' })
return displayNames.of(code.toUpperCase()) || code
} catch {
return code
}
}
/** Convert SCREAMING_SNAKE_CASE to Title Case */
function formatLabel(value: string): string {
if (!value) return value
return value
.replace(/_/g, ' ')
.toLowerCase()
.replace(/\b\w/g, (c) => c.toUpperCase())
}
/** Custom tooltip for the pie chart */
function CountryTooltip({ active, payload }: { active?: boolean; payload?: Array<{ payload: { country: string; count: number; percentage: number } }> }) {
if (!active || !payload?.length) return null
const d = payload[0].payload
return (
<div className="rounded-md border bg-card px-3 py-2 text-sm shadow-md">
<p className="font-medium">{getCountryName(d.country)}</p>
<p className="text-muted-foreground">{d.count} projects ({d.percentage.toFixed(1)}%)</p>
</div>
)
}
/** Custom tooltip for bar charts */
function BarTooltip({ active, payload, labelFormatter }: { active?: boolean; payload?: Array<{ value: number }>; label?: string; labelFormatter: (val: string) => string }) {
if (!active || !payload?.length) return null
const entry = payload[0]
const rawPayload = entry as unknown as { payload: Record<string, unknown> }
const dataPoint = rawPayload.payload
const rawLabel = (dataPoint.category || dataPoint.issue || '') as string
return (
<div className="rounded-md border bg-card px-3 py-2 text-sm shadow-md">
<p className="font-medium">{labelFormatter(rawLabel)}</p>
<p className="text-muted-foreground">{entry.value} projects</p>
</div>
)
}
export function DiversityMetricsChart({ data }: DiversityMetricsProps) {
if (data.total === 0) {
return (
<Card>
<CardContent className="flex items-center justify-center py-12">
<p className="text-muted-foreground">No project data available</p>
</CardContent>
</Card>
)
}
// Top countries for pie chart (max 10, others grouped)
const topCountries = data.byCountry.slice(0, 10)
const otherCountries = data.byCountry.slice(10)
const countryPieData = otherCountries.length > 0
? [...topCountries, {
country: 'Others',
count: otherCountries.reduce((sum, c) => sum + c.count, 0),
percentage: otherCountries.reduce((sum, c) => sum + c.percentage, 0),
}]
: topCountries
// Pre-format category and ocean issue data for display
const formattedCategories = data.byCategory.slice(0, 10).map((c) => ({
...c,
category: formatLabel(c.category),
}))
const formattedOceanIssues = data.byOceanIssue.slice(0, 15).map((o) => ({
...o,
issue: formatLabel(o.issue),
}))
return (
<div className="space-y-6">
{/* Summary */}
<div className="grid gap-4 sm:grid-cols-2 lg:grid-cols-4">
<Card>
<CardContent className="pt-6">
<div className="text-2xl font-bold">{data.total}</div>
<p className="text-sm text-muted-foreground">Total Projects</p>
</CardContent>
</Card>
<Card>
<CardContent className="pt-6">
<div className="text-2xl font-bold">{data.byCountry.length}</div>
<p className="text-sm text-muted-foreground">Countries Represented</p>
</CardContent>
</Card>
<Card>
<CardContent className="pt-6">
<div className="text-2xl font-bold">{data.byCategory.length}</div>
<p className="text-sm text-muted-foreground">Categories</p>
</CardContent>
</Card>
<Card>
<CardContent className="pt-6">
<div className="text-2xl font-bold">{data.byTag.length}</div>
<p className="text-sm text-muted-foreground">Unique Tags</p>
</CardContent>
</Card>
</div>
<div className="grid gap-6 lg:grid-cols-2">
{/* Country Distribution */}
<Card>
<CardHeader>
<CardTitle>Geographic Distribution</CardTitle>
</CardHeader>
<CardContent>
<div className="h-[400px]">
<ResponsiveContainer width="100%" height="100%">
<PieChart>
<Pie
data={countryPieData}
cx="50%"
cy="50%"
innerRadius={60}
outerRadius={120}
paddingAngle={2}
dataKey="count"
nameKey="country"
label={((props: unknown) => {
const p = props as { country: string; percentage: number }
return `${getCountryName(p.country)} (${p.percentage.toFixed(0)}%)`
}) as unknown as boolean}
fontSize={13}
>
{countryPieData.map((_, index) => (
<Cell key={`cell-${index}`} fill={PIE_COLORS[index % PIE_COLORS.length]} />
))}
</Pie>
<Tooltip content={<CountryTooltip />} />
<Legend
formatter={(value: string) => getCountryName(value)}
wrapperStyle={{ fontSize: '13px' }}
/>
</PieChart>
</ResponsiveContainer>
</div>
</CardContent>
</Card>
{/* Category Distribution */}
<Card>
<CardHeader>
<CardTitle>Competition Categories</CardTitle>
</CardHeader>
<CardContent>
{formattedCategories.length > 0 ? (
<div className="h-[400px]">
<ResponsiveContainer width="100%" height="100%">
<BarChart
data={formattedCategories}
layout="vertical"
margin={{ top: 5, right: 30, bottom: 5, left: 120 }}
>
<CartesianGrid strokeDasharray="3 3" className="stroke-muted" />
<XAxis type="number" tick={{ fontSize: 13 }} />
<YAxis
type="category"
dataKey="category"
width={110}
tick={{ fontSize: 13 }}
/>
<Tooltip content={<BarTooltip labelFormatter={(v) => v} />} />
<Bar dataKey="count" fill="#053d57" radius={[0, 4, 4, 0]} />
</BarChart>
</ResponsiveContainer>
</div>
) : (
<p className="text-muted-foreground text-center py-8">No category data</p>
)}
</CardContent>
</Card>
</div>
{/* Ocean Issues */}
{formattedOceanIssues.length > 0 && (
<Card>
<CardHeader>
<CardTitle>Ocean Issues Addressed</CardTitle>
</CardHeader>
<CardContent>
<div className="h-[400px]">
<ResponsiveContainer width="100%" height="100%">
<BarChart
data={formattedOceanIssues}
margin={{ top: 20, right: 30, bottom: 80, left: 20 }}
>
<CartesianGrid strokeDasharray="3 3" className="stroke-muted" />
<XAxis
dataKey="issue"
angle={-35}
textAnchor="end"
height={100}
tick={{ fontSize: 12 }}
interval={0}
/>
<YAxis tick={{ fontSize: 13 }} />
<Tooltip content={<BarTooltip labelFormatter={(v) => v} />} />
<Bar dataKey="count" fill="#557f8c" radius={[4, 4, 0, 0]} />
</BarChart>
</ResponsiveContainer>
</div>
</CardContent>
</Card>
)}
{/* Tags Cloud */}
{data.byTag.length > 0 && (
<Card>
<CardHeader>
<CardTitle>Project Tags</CardTitle>
</CardHeader>
<CardContent>
<div className="flex flex-wrap gap-2">
{data.byTag.slice(0, 30).map((tag) => (
<Badge
key={tag.tag}
variant="secondary"
className="text-sm"
style={{
fontSize: `${Math.max(0.75, Math.min(1.4, 0.75 + tag.percentage / 20))}rem`,
}}
>
{tag.tag} ({tag.count})
</Badge>
))}
</div>
</CardContent>
</Card>
)}
</div>
)
}
'use client'
import {
PieChart,
Pie,
Cell,
Tooltip,
ResponsiveContainer,
Legend,
BarChart,
Bar,
XAxis,
YAxis,
CartesianGrid,
} from 'recharts'
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card'
import { Badge } from '@/components/ui/badge'
interface DiversityData {
total: number
byCountry: { country: string; count: number; percentage: number }[]
byCategory: { category: string; count: number; percentage: number }[]
byOceanIssue: { issue: string; count: number; percentage: number }[]
byTag: { tag: string; count: number; percentage: number }[]
}
interface DiversityMetricsProps {
data: DiversityData
}
const PIE_COLORS = [
'#053d57', '#de0f1e', '#557f8c', '#f38a52', '#6ad82f',
'#3be31e', '#c9c052', '#e6382f', '#ed6141', '#0bd90f',
'#8884d8', '#82ca9d', '#ffc658', '#ff7c7c', '#8dd1e1',
]
/** Convert ISO 3166-1 alpha-2 code to full country name using Intl API */
function getCountryName(code: string): string {
if (code === 'Others') return 'Others'
try {
const displayNames = new Intl.DisplayNames(['en'], { type: 'region' })
return displayNames.of(code.toUpperCase()) || code
} catch {
return code
}
}
/** Convert SCREAMING_SNAKE_CASE to Title Case */
function formatLabel(value: string): string {
if (!value) return value
return value
.replace(/_/g, ' ')
.toLowerCase()
.replace(/\b\w/g, (c) => c.toUpperCase())
}
/** Custom tooltip for the pie chart */
function CountryTooltip({ active, payload }: { active?: boolean; payload?: Array<{ payload: { country: string; count: number; percentage: number } }> }) {
if (!active || !payload?.length) return null
const d = payload[0].payload
return (
<div className="rounded-md border bg-card px-3 py-2 text-sm shadow-md">
<p className="font-medium">{getCountryName(d.country)}</p>
<p className="text-muted-foreground">{d.count} projects ({d.percentage.toFixed(1)}%)</p>
</div>
)
}
/** Custom tooltip for bar charts */
function BarTooltip({ active, payload, labelFormatter }: { active?: boolean; payload?: Array<{ value: number }>; label?: string; labelFormatter: (val: string) => string }) {
if (!active || !payload?.length) return null
const entry = payload[0]
const rawPayload = entry as unknown as { payload: Record<string, unknown> }
const dataPoint = rawPayload.payload
const rawLabel = (dataPoint.category || dataPoint.issue || '') as string
return (
<div className="rounded-md border bg-card px-3 py-2 text-sm shadow-md">
<p className="font-medium">{labelFormatter(rawLabel)}</p>
<p className="text-muted-foreground">{entry.value} projects</p>
</div>
)
}
export function DiversityMetricsChart({ data }: DiversityMetricsProps) {
if (data.total === 0) {
return (
<Card>
<CardContent className="flex items-center justify-center py-12">
<p className="text-muted-foreground">No project data available</p>
</CardContent>
</Card>
)
}
// Top countries for pie chart (max 10, others grouped)
const topCountries = data.byCountry.slice(0, 10)
const otherCountries = data.byCountry.slice(10)
const countryPieData = otherCountries.length > 0
? [...topCountries, {
country: 'Others',
count: otherCountries.reduce((sum, c) => sum + c.count, 0),
percentage: otherCountries.reduce((sum, c) => sum + c.percentage, 0),
}]
: topCountries
// Pre-format category and ocean issue data for display
const formattedCategories = data.byCategory.slice(0, 10).map((c) => ({
...c,
category: formatLabel(c.category),
}))
const formattedOceanIssues = data.byOceanIssue.slice(0, 15).map((o) => ({
...o,
issue: formatLabel(o.issue),
}))
return (
<div className="space-y-6">
{/* Summary */}
<div className="grid gap-4 sm:grid-cols-2 lg:grid-cols-4">
<Card>
<CardContent className="pt-6">
<div className="text-2xl font-bold">{data.total}</div>
<p className="text-sm text-muted-foreground">Total Projects</p>
</CardContent>
</Card>
<Card>
<CardContent className="pt-6">
<div className="text-2xl font-bold">{data.byCountry.length}</div>
<p className="text-sm text-muted-foreground">Countries Represented</p>
</CardContent>
</Card>
<Card>
<CardContent className="pt-6">
<div className="text-2xl font-bold">{data.byCategory.length}</div>
<p className="text-sm text-muted-foreground">Categories</p>
</CardContent>
</Card>
<Card>
<CardContent className="pt-6">
<div className="text-2xl font-bold">{data.byTag.length}</div>
<p className="text-sm text-muted-foreground">Unique Tags</p>
</CardContent>
</Card>
</div>
<div className="grid gap-6 lg:grid-cols-2">
{/* Country Distribution */}
<Card>
<CardHeader>
<CardTitle>Geographic Distribution</CardTitle>
</CardHeader>
<CardContent>
<div className="h-[400px]">
<ResponsiveContainer width="100%" height="100%">
<PieChart>
<Pie
data={countryPieData}
cx="50%"
cy="50%"
innerRadius={60}
outerRadius={120}
paddingAngle={2}
dataKey="count"
nameKey="country"
label={((props: unknown) => {
const p = props as { country: string; percentage: number }
return `${getCountryName(p.country)} (${p.percentage.toFixed(0)}%)`
}) as unknown as boolean}
fontSize={13}
>
{countryPieData.map((_, index) => (
<Cell key={`cell-${index}`} fill={PIE_COLORS[index % PIE_COLORS.length]} />
))}
</Pie>
<Tooltip content={<CountryTooltip />} />
<Legend
formatter={(value: string) => getCountryName(value)}
wrapperStyle={{ fontSize: '13px' }}
/>
</PieChart>
</ResponsiveContainer>
</div>
</CardContent>
</Card>
{/* Category Distribution */}
<Card>
<CardHeader>
<CardTitle>Competition Categories</CardTitle>
</CardHeader>
<CardContent>
{formattedCategories.length > 0 ? (
<div className="h-[400px]">
<ResponsiveContainer width="100%" height="100%">
<BarChart
data={formattedCategories}
layout="vertical"
margin={{ top: 5, right: 30, bottom: 5, left: 120 }}
>
<CartesianGrid strokeDasharray="3 3" className="stroke-muted" />
<XAxis type="number" tick={{ fontSize: 13 }} />
<YAxis
type="category"
dataKey="category"
width={110}
tick={{ fontSize: 13 }}
/>
<Tooltip content={<BarTooltip labelFormatter={(v) => v} />} />
<Bar dataKey="count" fill="#053d57" radius={[0, 4, 4, 0]} />
</BarChart>
</ResponsiveContainer>
</div>
) : (
<p className="text-muted-foreground text-center py-8">No category data</p>
)}
</CardContent>
</Card>
</div>
{/* Ocean Issues */}
{formattedOceanIssues.length > 0 && (
<Card>
<CardHeader>
<CardTitle>Ocean Issues Addressed</CardTitle>
</CardHeader>
<CardContent>
<div className="h-[400px]">
<ResponsiveContainer width="100%" height="100%">
<BarChart
data={formattedOceanIssues}
margin={{ top: 20, right: 30, bottom: 80, left: 20 }}
>
<CartesianGrid strokeDasharray="3 3" className="stroke-muted" />
<XAxis
dataKey="issue"
angle={-35}
textAnchor="end"
height={100}
tick={{ fontSize: 12 }}
interval={0}
/>
<YAxis tick={{ fontSize: 13 }} />
<Tooltip content={<BarTooltip labelFormatter={(v) => v} />} />
<Bar dataKey="count" fill="#557f8c" radius={[4, 4, 0, 0]} />
</BarChart>
</ResponsiveContainer>
</div>
</CardContent>
</Card>
)}
{/* Tags Cloud */}
{data.byTag.length > 0 && (
<Card>
<CardHeader>
<CardTitle>Project Tags</CardTitle>
</CardHeader>
<CardContent>
<div className="flex flex-wrap gap-2">
{data.byTag.slice(0, 30).map((tag) => (
<Badge
key={tag.tag}
variant="secondary"
className="text-sm"
style={{
fontSize: `${Math.max(0.75, Math.min(1.4, 0.75 + tag.percentage / 20))}rem`,
}}
>
{tag.tag} ({tag.count})
</Badge>
))}
</div>
</CardContent>
</Card>
)}
</div>
)
}

View File

@@ -1,12 +1,12 @@
export { ScoreDistributionChart } from './score-distribution'
export { EvaluationTimelineChart } from './evaluation-timeline'
export { StatusBreakdownChart } from './status-breakdown'
export { JurorWorkloadChart } from './juror-workload'
export { ProjectRankingsChart } from './project-rankings'
export { CriteriaScoresChart } from './criteria-scores'
export { GeographicDistribution } from './geographic-distribution'
export { GeographicSummaryCard } from './geographic-summary-card'
// Advanced analytics charts (F10)
export { CrossStageComparisonChart } from './cross-round-comparison'
export { JurorConsistencyChart } from './juror-consistency'
export { DiversityMetricsChart } from './diversity-metrics'
export { ScoreDistributionChart } from './score-distribution'
export { EvaluationTimelineChart } from './evaluation-timeline'
export { StatusBreakdownChart } from './status-breakdown'
export { JurorWorkloadChart } from './juror-workload'
export { ProjectRankingsChart } from './project-rankings'
export { CriteriaScoresChart } from './criteria-scores'
export { GeographicDistribution } from './geographic-distribution'
export { GeographicSummaryCard } from './geographic-summary-card'
// Advanced analytics charts (F10)
export { CrossStageComparisonChart } from './cross-round-comparison'
export { JurorConsistencyChart } from './juror-consistency'
export { DiversityMetricsChart } from './diversity-metrics'

View File

@@ -1,171 +1,171 @@
'use client'
import {
ScatterChart,
Scatter,
XAxis,
YAxis,
CartesianGrid,
Tooltip,
ResponsiveContainer,
ReferenceLine,
} from 'recharts'
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card'
import { Badge } from '@/components/ui/badge'
import {
Table,
TableBody,
TableCell,
TableHead,
TableHeader,
TableRow,
} from '@/components/ui/table'
import { AlertTriangle } from 'lucide-react'
interface JurorMetric {
userId: string
name: string
email: string
evaluationCount: number
averageScore: number
stddev: number
deviationFromOverall: number
isOutlier: boolean
}
interface JurorConsistencyProps {
data: {
overallAverage: number
jurors: JurorMetric[]
}
}
export function JurorConsistencyChart({ data }: JurorConsistencyProps) {
const scatterData = data.jurors.map((j) => ({
name: j.name,
avgScore: parseFloat(j.averageScore.toFixed(2)),
stddev: parseFloat(j.stddev.toFixed(2)),
evaluations: j.evaluationCount,
isOutlier: j.isOutlier,
}))
const outlierCount = data.jurors.filter((j) => j.isOutlier).length
return (
<div className="space-y-6">
{/* Scatter: Average Score vs Standard Deviation */}
<Card>
<CardHeader>
<CardTitle className="flex items-center justify-between">
<span>Juror Scoring Patterns</span>
<span className="text-sm font-normal text-muted-foreground">
Overall Avg: {data.overallAverage.toFixed(2)}
{outlierCount > 0 && (
<Badge variant="destructive" className="ml-2">
{outlierCount} outlier{outlierCount > 1 ? 's' : ''}
</Badge>
)}
</span>
</CardTitle>
</CardHeader>
<CardContent>
<div className="h-[400px]">
<ResponsiveContainer width="100%" height="100%">
<ScatterChart margin={{ top: 20, right: 20, bottom: 20, left: 20 }}>
<CartesianGrid strokeDasharray="3 3" className="stroke-muted" />
<XAxis
type="number"
dataKey="avgScore"
name="Average Score"
domain={[0, 10]}
label={{ value: 'Average Score', position: 'insideBottom', offset: -10 }}
/>
<YAxis
type="number"
dataKey="stddev"
name="Std Deviation"
label={{ value: 'Std Deviation', angle: -90, position: 'insideLeft' }}
/>
<Tooltip
contentStyle={{
backgroundColor: 'hsl(var(--card))',
border: '1px solid hsl(var(--border))',
borderRadius: '6px',
}}
/>
<ReferenceLine
x={data.overallAverage}
stroke="#de0f1e"
strokeDasharray="3 3"
label={{ value: 'Avg', fill: '#de0f1e', position: 'top' }}
/>
<Scatter data={scatterData} fill="#053d57">
{scatterData.map((entry, index) => (
<circle
key={index}
r={Math.max(4, entry.evaluations)}
fill={entry.isOutlier ? '#de0f1e' : '#053d57'}
fillOpacity={0.7}
/>
))}
</Scatter>
</ScatterChart>
</ResponsiveContainer>
</div>
<p className="text-xs text-muted-foreground mt-2 text-center">
Dot size represents number of evaluations. Red dots indicate outlier jurors (2+ points from mean).
</p>
</CardContent>
</Card>
{/* Juror details table */}
<Card>
<CardHeader>
<CardTitle>Juror Consistency Details</CardTitle>
</CardHeader>
<CardContent>
<Table>
<TableHeader>
<TableRow>
<TableHead>Juror</TableHead>
<TableHead className="text-right">Evaluations</TableHead>
<TableHead className="text-right">Avg Score</TableHead>
<TableHead className="text-right">Std Dev</TableHead>
<TableHead className="text-right">Deviation from Mean</TableHead>
<TableHead className="text-center">Status</TableHead>
</TableRow>
</TableHeader>
<TableBody>
{data.jurors.map((juror) => (
<TableRow key={juror.userId} className={juror.isOutlier ? 'bg-destructive/5' : ''}>
<TableCell>
<div>
<p className="font-medium">{juror.name}</p>
<p className="text-xs text-muted-foreground">{juror.email}</p>
</div>
</TableCell>
<TableCell className="text-right tabular-nums">{juror.evaluationCount}</TableCell>
<TableCell className="text-right tabular-nums">{juror.averageScore.toFixed(2)}</TableCell>
<TableCell className="text-right tabular-nums">{juror.stddev.toFixed(2)}</TableCell>
<TableCell className="text-right tabular-nums">
{juror.deviationFromOverall.toFixed(2)}
</TableCell>
<TableCell className="text-center">
{juror.isOutlier ? (
<Badge variant="destructive" className="gap-1">
<AlertTriangle className="h-3 w-3" />
Outlier
</Badge>
) : (
<Badge variant="secondary">Normal</Badge>
)}
</TableCell>
</TableRow>
))}
</TableBody>
</Table>
</CardContent>
</Card>
</div>
)
}
'use client'
import {
ScatterChart,
Scatter,
XAxis,
YAxis,
CartesianGrid,
Tooltip,
ResponsiveContainer,
ReferenceLine,
} from 'recharts'
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card'
import { Badge } from '@/components/ui/badge'
import {
Table,
TableBody,
TableCell,
TableHead,
TableHeader,
TableRow,
} from '@/components/ui/table'
import { AlertTriangle } from 'lucide-react'
interface JurorMetric {
userId: string
name: string
email: string
evaluationCount: number
averageScore: number
stddev: number
deviationFromOverall: number
isOutlier: boolean
}
interface JurorConsistencyProps {
data: {
overallAverage: number
jurors: JurorMetric[]
}
}
export function JurorConsistencyChart({ data }: JurorConsistencyProps) {
const scatterData = data.jurors.map((j) => ({
name: j.name,
avgScore: parseFloat(j.averageScore.toFixed(2)),
stddev: parseFloat(j.stddev.toFixed(2)),
evaluations: j.evaluationCount,
isOutlier: j.isOutlier,
}))
const outlierCount = data.jurors.filter((j) => j.isOutlier).length
return (
<div className="space-y-6">
{/* Scatter: Average Score vs Standard Deviation */}
<Card>
<CardHeader>
<CardTitle className="flex items-center justify-between">
<span>Juror Scoring Patterns</span>
<span className="text-sm font-normal text-muted-foreground">
Overall Avg: {data.overallAverage.toFixed(2)}
{outlierCount > 0 && (
<Badge variant="destructive" className="ml-2">
{outlierCount} outlier{outlierCount > 1 ? 's' : ''}
</Badge>
)}
</span>
</CardTitle>
</CardHeader>
<CardContent>
<div className="h-[400px]">
<ResponsiveContainer width="100%" height="100%">
<ScatterChart margin={{ top: 20, right: 20, bottom: 20, left: 20 }}>
<CartesianGrid strokeDasharray="3 3" className="stroke-muted" />
<XAxis
type="number"
dataKey="avgScore"
name="Average Score"
domain={[0, 10]}
label={{ value: 'Average Score', position: 'insideBottom', offset: -10 }}
/>
<YAxis
type="number"
dataKey="stddev"
name="Std Deviation"
label={{ value: 'Std Deviation', angle: -90, position: 'insideLeft' }}
/>
<Tooltip
contentStyle={{
backgroundColor: 'hsl(var(--card))',
border: '1px solid hsl(var(--border))',
borderRadius: '6px',
}}
/>
<ReferenceLine
x={data.overallAverage}
stroke="#de0f1e"
strokeDasharray="3 3"
label={{ value: 'Avg', fill: '#de0f1e', position: 'top' }}
/>
<Scatter data={scatterData} fill="#053d57">
{scatterData.map((entry, index) => (
<circle
key={index}
r={Math.max(4, entry.evaluations)}
fill={entry.isOutlier ? '#de0f1e' : '#053d57'}
fillOpacity={0.7}
/>
))}
</Scatter>
</ScatterChart>
</ResponsiveContainer>
</div>
<p className="text-xs text-muted-foreground mt-2 text-center">
Dot size represents number of evaluations. Red dots indicate outlier jurors (2+ points from mean).
</p>
</CardContent>
</Card>
{/* Juror details table */}
<Card>
<CardHeader>
<CardTitle>Juror Consistency Details</CardTitle>
</CardHeader>
<CardContent>
<Table>
<TableHeader>
<TableRow>
<TableHead>Juror</TableHead>
<TableHead className="text-right">Evaluations</TableHead>
<TableHead className="text-right">Avg Score</TableHead>
<TableHead className="text-right">Std Dev</TableHead>
<TableHead className="text-right">Deviation from Mean</TableHead>
<TableHead className="text-center">Status</TableHead>
</TableRow>
</TableHeader>
<TableBody>
{data.jurors.map((juror) => (
<TableRow key={juror.userId} className={juror.isOutlier ? 'bg-destructive/5' : ''}>
<TableCell>
<div>
<p className="font-medium">{juror.name}</p>
<p className="text-xs text-muted-foreground">{juror.email}</p>
</div>
</TableCell>
<TableCell className="text-right tabular-nums">{juror.evaluationCount}</TableCell>
<TableCell className="text-right tabular-nums">{juror.averageScore.toFixed(2)}</TableCell>
<TableCell className="text-right tabular-nums">{juror.stddev.toFixed(2)}</TableCell>
<TableCell className="text-right tabular-nums">
{juror.deviationFromOverall.toFixed(2)}
</TableCell>
<TableCell className="text-center">
{juror.isOutlier ? (
<Badge variant="destructive" className="gap-1">
<AlertTriangle className="h-3 w-3" />
Outlier
</Badge>
) : (
<Badge variant="secondary">Normal</Badge>
)}
</TableCell>
</TableRow>
))}
</TableBody>
</Table>
</CardContent>
</Card>
</div>
)
}