@@ -12,88 +12,167 @@ import {
1212 ChartContainer ,
1313 ChartLegend ,
1414 ChartLegendContent ,
15+ ChartTooltip ,
16+ ChartTooltipContent ,
1517} from "@/components/ui/chart" ;
16- import { Pie , PieChart } from "recharts" ;
1718import { cn } from "@/lib/utils" ;
19+ import { Cell , Pie , PieChart } from "recharts" ;
1820
1921interface ApplicantStatusCounts extends Record < string , number > { }
2022
2123interface AdminDashboardApplicantStatusPieProps {
2224 data ?: ApplicantStatusCounts ;
2325}
2426
27+ // Distinct, accessible colors per status — avoids the all-blue sameness
28+ const STATUS_COLORS : Record < string , string > = {
29+ applying : "#3b82f6" , // blue
30+ testing : "#f59e0b" , // amber
31+ onboarding : "#10b981" , // emerald
32+ passed : "#8b5cf6" , // violet
33+ } ;
34+
35+ // Fallback for any unexpected statuses
36+ const FALLBACK_COLORS = [ "#ec4899" , "#14b8a6" , "#f97316" , "#6366f1" ] ;
37+
2538export default function AdminDashboardApplicantStatusPie ( {
2639 data,
2740} : AdminDashboardApplicantStatusPieProps ) {
2841 const title = "Total Applicant Status" ;
29-
30- if ( ! data )
31- return (
32- < Card className = { cn (
42+
43+ const emptyCard = (
44+ < Card
45+ className = { cn (
3346 "flex h-full flex-col relative overflow-hidden" ,
3447 "bg-white dark:bg-gradient-to-br dark:from-blue-950 dark:via-slate-900 dark:to-slate-950" ,
35- "border-gray-200 dark:border-blue-900/30"
36- ) } >
37- < div className = "absolute inset-0 bg-gradient-to-br to-transparent pointer-events-none opacity-0 dark:opacity-100 dark:from-blue-500/10" />
38-
39- < CardHeader className = "relative items-center pb-0" >
40- < CardTitle className = "text-lg" > { title } </ CardTitle >
41- </ CardHeader >
42- < CardContent className = "relative flex flex-1 items-center justify-center" >
43- < p className = "text-muted-foreground" > No data available</ p >
44- </ CardContent >
45- </ Card >
46- ) ;
47-
48- const chartData = Object . keys ( data || { } ) . map ( ( status ) => ( {
48+ "border-gray-200 dark:border-blue-900/30" ,
49+ ) }
50+ >
51+ < div className = "absolute inset-0 bg-gradient-to-br to-transparent pointer-events-none opacity-0 dark:opacity-100 dark:from-blue-500/10" />
52+ < CardHeader className = "relative items-center pb-0" >
53+ < CardTitle className = "text-lg" > { title } </ CardTitle >
54+ </ CardHeader >
55+ < CardContent className = "relative flex flex-1 items-center justify-center" >
56+ < p className = "text-muted-foreground" > No data available</ p >
57+ </ CardContent >
58+ </ Card >
59+ ) ;
60+
61+ if ( ! data ) return emptyCard ;
62+
63+ const total = Object . values ( data ) . reduce ( ( sum , v ) => sum + v , 0 ) ;
64+
65+ if ( total === 0 ) return emptyCard ;
66+
67+ const chartData = Object . keys ( data ) . map ( ( status , index ) => ( {
4968 status,
5069 applicants : data [ status ] ,
51- fill : `var(--color-${ status } )` ,
70+ percentage : total > 0 ? ( ( ( data [ status ] ?? 0 ) / total ) * 100 ) . toFixed ( 1 ) : "0" ,
71+ fill : STATUS_COLORS [ status ] ?? FALLBACK_COLORS [ index % FALLBACK_COLORS . length ] ,
5272 } ) ) ;
5373
54- const chartConfig = Object . keys ( data || { } ) . reduce (
55- ( config , status ) => {
56- const capitalizedLabel = status . charAt ( 0 ) . toUpperCase ( ) + status . slice ( 1 ) ;
74+ const chartConfig = Object . keys ( data ) . reduce (
75+ ( config , status , index ) => {
5776 config [ status ] = {
58- label : capitalizedLabel ,
59- color : `hsl(var(--chart- ${ Object . keys ( config ) . length + 1 } ))` ,
77+ label : status . charAt ( 0 ) . toUpperCase ( ) + status . slice ( 1 ) ,
78+ color : STATUS_COLORS [ status ] ?? FALLBACK_COLORS [ index % FALLBACK_COLORS . length ] ,
6079 } ;
6180 return config ;
6281 } ,
63- { } as Record < string , { label : string ; color : string } > ,
82+ { } as ChartConfig ,
6483 ) ;
6584
6685 return (
67- < Card className = { cn (
68- "flex h-full flex-col relative overflow-hidden" ,
69- "bg-white dark:bg-gradient-to-br dark:from-blue-950 dark:via-slate-900 dark:to-slate-950" ,
70- "border-gray-200 dark:border-blue-900/30"
71- ) } >
86+ < Card
87+ className = { cn (
88+ "flex h-full flex-col relative overflow-hidden" ,
89+ "bg-white dark:bg-gradient-to-br dark:from-blue-950 dark:via-slate-900 dark:to-slate-950" ,
90+ "border-gray-200 dark:border-blue-900/30" ,
91+ ) }
92+ >
7293 < div className = "absolute inset-0 bg-gradient-to-br to-transparent pointer-events-none opacity-0 dark:opacity-100 dark:from-blue-500/10" />
73-
94+
7495 < CardHeader className = "relative items-center pb-0" >
7596 < CardTitle className = "text-lg" > { title } </ CardTitle >
7697 < CardDescription className = "text-center text-sm" >
7798 Distribution of application statuses
7899 </ CardDescription >
79100 </ CardHeader >
80- < CardContent className = "relative flex-1 pb-0" >
101+
102+ < CardContent className = "relative flex-1 pb-6" >
81103 < ChartContainer
82104 config = { chartConfig }
83- className = "mx-auto aspect-square max-h-[350px ]"
105+ className = "mx-auto aspect-square max-h-[280px ]"
84106 >
85107 < PieChart >
86- < Pie
87- data = { chartData }
88- dataKey = "applicants"
89- strokeWidth = { 2 }
108+ { /* Tooltip: shows status label, count, and percentage */ }
109+ < ChartTooltip
110+ content = {
111+ < ChartTooltipContent
112+ formatter = { ( value , name ) => {
113+ const entry = chartData . find ( ( d ) => d . status === name ) ;
114+ const percentage = entry ?. percentage ?? "0" ;
115+ return (
116+ < div className = "flex flex-col gap-0.5" >
117+ < span className = "font-semibold capitalize" > { name } </ span >
118+ < span >
119+ { value } applicants ({ percentage } %)
120+ </ span >
121+ </ div >
122+ ) ;
123+ } }
124+ hideLabel
125+ />
126+ }
90127 />
128+
129+ { /* Donut chart — inner radius reveals the total count in the center */ }
130+ < Pie
131+ data = { chartData }
132+ dataKey = "applicants"
133+ nameKey = "status"
134+ innerRadius = "50%"
135+ outerRadius = "75%"
136+ strokeWidth = { 3 }
137+ stroke = "transparent"
138+ paddingAngle = { 3 }
139+ labelLine = { false }
140+ label = { ( { cx, cy } ) => (
141+ < g >
142+ < text
143+ x = { cx }
144+ y = { cy - 8 }
145+ textAnchor = "middle"
146+ dominantBaseline = "middle"
147+ className = "fill-foreground"
148+ style = { { fontSize : "1.5rem" , fontWeight : 700 } }
149+ >
150+ { total . toLocaleString ( ) }
151+ </ text >
152+ < text
153+ x = { cx }
154+ y = { cy + 14 }
155+ textAnchor = "middle"
156+ dominantBaseline = "middle"
157+ style = { { fontSize : "0.75rem" , fill : "hsl(var(--muted-foreground))" } }
158+ >
159+ Total
160+ </ text >
161+ </ g >
162+ ) }
163+ >
164+ { chartData . map ( ( entry ) => (
165+ < Cell key = { entry . status } fill = { entry . fill } />
166+ ) ) }
167+ </ Pie >
168+
91169 < ChartLegend
92170 content = { < ChartLegendContent nameKey = "status" /> }
93171 className = "-translate-y-2 flex-wrap gap-2 [&>*]:basis-1/4 [&>*]:justify-center"
94172 />
95173 </ PieChart >
96174 </ ChartContainer >
175+
97176 </ CardContent >
98177 </ Card >
99178 ) ;
0 commit comments