122 lines
3.6 KiB
TypeScript
122 lines
3.6 KiB
TypeScript
|
|
'use client'
|
||
|
|
|
||
|
|
import {
|
||
|
|
BarChart,
|
||
|
|
Bar,
|
||
|
|
XAxis,
|
||
|
|
YAxis,
|
||
|
|
CartesianGrid,
|
||
|
|
Tooltip,
|
||
|
|
ResponsiveContainer,
|
||
|
|
Cell,
|
||
|
|
ReferenceLine,
|
||
|
|
} from 'recharts'
|
||
|
|
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card'
|
||
|
|
|
||
|
|
interface ProjectRankingData {
|
||
|
|
id: string
|
||
|
|
title: string
|
||
|
|
teamName: string | null
|
||
|
|
status: string
|
||
|
|
averageScore: number | null
|
||
|
|
evaluationCount: number
|
||
|
|
}
|
||
|
|
|
||
|
|
interface ProjectRankingsProps {
|
||
|
|
data: ProjectRankingData[]
|
||
|
|
limit?: number
|
||
|
|
}
|
||
|
|
|
||
|
|
// Generate color based on score (red to green gradient)
|
||
|
|
const getScoreColor = (score: number): string => {
|
||
|
|
if (score >= 8) return '#0bd90f' // Excellent - green
|
||
|
|
if (score >= 6) return '#82ca9d' // Good - light green
|
||
|
|
if (score >= 4) return '#ffc658' // Average - yellow
|
||
|
|
if (score >= 2) return '#ff7300' // Poor - orange
|
||
|
|
return '#de0f1e' // Very poor - red
|
||
|
|
}
|
||
|
|
|
||
|
|
export function ProjectRankingsChart({
|
||
|
|
data,
|
||
|
|
limit = 20,
|
||
|
|
}: ProjectRankingsProps) {
|
||
|
|
const displayData = data.slice(0, limit).map((d, index) => ({
|
||
|
|
...d,
|
||
|
|
rank: index + 1,
|
||
|
|
displayTitle:
|
||
|
|
d.title.length > 25 ? d.title.substring(0, 25) + '...' : d.title,
|
||
|
|
score: d.averageScore || 0,
|
||
|
|
}))
|
||
|
|
|
||
|
|
const averageScore =
|
||
|
|
data.length > 0
|
||
|
|
? data.reduce((sum, d) => sum + (d.averageScore || 0), 0) / data.length
|
||
|
|
: 0
|
||
|
|
|
||
|
|
return (
|
||
|
|
<Card>
|
||
|
|
<CardHeader>
|
||
|
|
<CardTitle className="flex items-center justify-between">
|
||
|
|
<span>Project Rankings</span>
|
||
|
|
<span className="text-sm font-normal text-muted-foreground">
|
||
|
|
Top {displayData.length} of {data.length} projects
|
||
|
|
</span>
|
||
|
|
</CardTitle>
|
||
|
|
</CardHeader>
|
||
|
|
<CardContent>
|
||
|
|
<div className="h-[500px]">
|
||
|
|
<ResponsiveContainer width="100%" height="100%">
|
||
|
|
<BarChart
|
||
|
|
data={displayData}
|
||
|
|
layout="vertical"
|
||
|
|
margin={{ top: 20, right: 30, bottom: 20, left: 150 }}
|
||
|
|
>
|
||
|
|
<CartesianGrid strokeDasharray="3 3" className="stroke-muted" />
|
||
|
|
<XAxis type="number" domain={[0, 10]} />
|
||
|
|
<YAxis
|
||
|
|
dataKey="displayTitle"
|
||
|
|
type="category"
|
||
|
|
width={140}
|
||
|
|
tick={{ fontSize: 11 }}
|
||
|
|
/>
|
||
|
|
<Tooltip
|
||
|
|
contentStyle={{
|
||
|
|
backgroundColor: 'hsl(var(--card))',
|
||
|
|
border: '1px solid hsl(var(--border))',
|
||
|
|
borderRadius: '6px',
|
||
|
|
}}
|
||
|
|
formatter={(value: number | undefined) => [(value ?? 0).toFixed(2), 'Average Score']}
|
||
|
|
labelFormatter={(_, payload) => {
|
||
|
|
if (payload && payload[0]) {
|
||
|
|
const item = payload[0].payload as ProjectRankingData & {
|
||
|
|
rank: number
|
||
|
|
}
|
||
|
|
return `#${item.rank} - ${item.title}${item.teamName ? ` (${item.teamName})` : ''}`
|
||
|
|
}
|
||
|
|
return ''
|
||
|
|
}}
|
||
|
|
/>
|
||
|
|
<ReferenceLine
|
||
|
|
x={averageScore}
|
||
|
|
stroke="#666"
|
||
|
|
strokeDasharray="5 5"
|
||
|
|
label={{
|
||
|
|
value: `Avg: ${averageScore.toFixed(1)}`,
|
||
|
|
position: 'top',
|
||
|
|
fill: '#666',
|
||
|
|
fontSize: 11,
|
||
|
|
}}
|
||
|
|
/>
|
||
|
|
<Bar dataKey="score" radius={[0, 4, 4, 0]}>
|
||
|
|
{displayData.map((entry, index) => (
|
||
|
|
<Cell key={`cell-${index}`} fill={getScoreColor(entry.score)} />
|
||
|
|
))}
|
||
|
|
</Bar>
|
||
|
|
</BarChart>
|
||
|
|
</ResponsiveContainer>
|
||
|
|
</div>
|
||
|
|
</CardContent>
|
||
|
|
</Card>
|
||
|
|
)
|
||
|
|
}
|