Files
TaskTracker/TaskTracker.Web/src/components/analytics/CategoryBreakdown.tsx

144 lines
4.4 KiB
TypeScript

import { useMemo } from 'react'
import { PieChart, Pie, Cell, ResponsiveContainer, Tooltip } from 'recharts'
import { useContextSummary } from '../../api/context'
import { CATEGORY_COLORS } from '../../lib/constants'
interface CategoryBreakdownProps {
minutes: number
taskId?: number
}
interface CategoryData {
name: string
count: number
color: string
percentage: number
}
export default function CategoryBreakdown({ minutes: _minutes, taskId: _taskId }: CategoryBreakdownProps) {
const { data: summary, isLoading } = useContextSummary()
const categories = useMemo(() => {
if (!summary) return []
// Aggregate by category
const catMap = new Map<string, number>()
for (const item of summary) {
const cat = item.category || 'Unknown'
catMap.set(cat, (catMap.get(cat) ?? 0) + item.eventCount)
}
const total = Array.from(catMap.values()).reduce((s, c) => s + c, 0)
const result: CategoryData[] = Array.from(catMap.entries())
.map(([name, count]) => ({
name,
count,
color: CATEGORY_COLORS[name] ?? CATEGORY_COLORS['Unknown'],
percentage: total > 0 ? Math.round((count / total) * 100) : 0,
}))
.sort((a, b) => b.count - a.count)
return result
}, [summary])
if (isLoading) {
return (
<div className="flex items-center justify-center h-64 text-[#94a3b8] text-sm">
Loading category breakdown...
</div>
)
}
if (categories.length === 0) {
return (
<div className="flex items-center justify-center h-64 text-[#94a3b8] text-sm">
No category data available.
</div>
)
}
const totalEvents = categories.reduce((s, c) => s + c.count, 0)
return (
<div className="flex gap-8 items-start">
{/* Left: Donut chart */}
<div className="w-56 h-56 shrink-0">
<ResponsiveContainer width="100%" height="100%">
<PieChart>
<Pie
data={categories}
dataKey="count"
nameKey="name"
cx="50%"
cy="50%"
innerRadius="60%"
outerRadius="80%"
paddingAngle={2}
stroke="none"
>
{categories.map((entry, index) => (
<Cell key={index} fill={entry.color} />
))}
</Pie>
<Tooltip
content={({ active, payload }) => {
if (!active || !payload || payload.length === 0) return null
const d = payload[0].payload as CategoryData
return (
<div
style={{
backgroundColor: '#1e293b',
border: '1px solid rgba(255,255,255,0.1)',
borderRadius: 8,
padding: '8px 12px',
}}
>
<div className="text-white text-sm font-medium">{d.name}</div>
<div className="text-[#94a3b8] text-xs mt-0.5">
{d.count} events ({d.percentage}%)
</div>
</div>
)
}}
/>
</PieChart>
</ResponsiveContainer>
</div>
{/* Right: Legend list */}
<div className="flex-1 space-y-3 pt-2">
{categories.map((cat) => (
<div key={cat.name} className="flex items-center gap-3">
{/* Colored dot */}
<span
className="w-2.5 h-2.5 rounded-full shrink-0"
style={{ backgroundColor: cat.color }}
/>
{/* Name + bar + stats */}
<div className="flex-1 min-w-0">
<div className="flex items-center justify-between mb-1">
<span className="text-sm text-white font-medium truncate">{cat.name}</span>
<span className="text-xs text-[#94a3b8] ml-2 shrink-0">
{cat.count} ({cat.percentage}%)
</span>
</div>
{/* Progress bar */}
<div className="h-1.5 rounded-full bg-white/5 overflow-hidden">
<div
className="h-full rounded-full transition-all duration-300"
style={{
width: `${totalEvents > 0 ? (cat.count / totalEvents) * 100 : 0}%`,
backgroundColor: cat.color,
}}
/>
</div>
</div>
</div>
))}
</div>
</div>
)
}