144 lines
4.4 KiB
TypeScript
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>
|
|
)
|
|
}
|