| 1 |
import React from "react"; |
| 2 |
import { |
| 3 |
PieChart, |
| 4 |
Pie, |
| 5 |
Cell, |
| 6 |
ResponsiveContainer, |
| 7 |
Tooltip, |
| 8 |
Legend, |
| 9 |
} from "recharts"; |
| 10 |
|
| 11 |
export interface BookingStatusPoint { |
| 12 |
label: string; |
| 13 |
value: number; |
| 14 |
color: string; |
| 15 |
} |
| 16 |
|
| 17 |
interface BookingStatusChartProps { |
| 18 |
data: BookingStatusPoint[]; |
| 19 |
} |
| 20 |
|
| 21 |
const BookingStatusChart: React.FC<BookingStatusChartProps> = ({ data }) => { |
| 22 |
const safeData = Array.isArray(data) ? data : []; |
| 23 |
|
| 24 |
const total = safeData.reduce((sum, item) => sum + (item.value || 0), 0); |
| 25 |
if (!safeData.length || total <= 0) { |
| 26 |
return ( |
| 27 |
<div className="flex items-center justify-center h-[140px] text-gray-500 dark:text-gray-400"> |
| 28 |
No data available |
| 29 |
</div> |
| 30 |
); |
| 31 |
} |
| 32 |
|
| 33 |
// Enrich data with percentage for legend/tooltip |
| 34 |
const chartData = safeData.map((item) => ({ |
| 35 |
...item, |
| 36 |
percent: total > 0 ? (item.value / total) * 100 : 0, |
| 37 |
})); |
| 38 |
|
| 39 |
const renderLegend: React.FC<any> = ({ payload }) => { |
| 40 |
if (!payload || !payload.length) return null; |
| 41 |
return ( |
| 42 |
<div className="flex flex-col gap-1 text-xs sm:text-sm ml-4"> |
| 43 |
{payload.map((entry: any, index: number) => { |
| 44 |
const color = entry.color; |
| 45 |
const label = entry.value as string; |
| 46 |
const percent = entry.payload?.percent ?? 0; |
| 47 |
const displayPercent = `${percent.toFixed(1)}%`; |
| 48 |
return ( |
| 49 |
<div key={index} className="flex items-center gap-2"> |
| 50 |
<span |
| 51 |
className="inline-block w-3 h-3 rounded" |
| 52 |
style={{ backgroundColor: color }} |
| 53 |
/> |
| 54 |
<span className="text-gray-700 dark:text-gray-300">{label}</span> |
| 55 |
<span className="ml-auto font-medium text-gray-900 dark:text-white"> |
| 56 |
{displayPercent} |
| 57 |
</span> |
| 58 |
</div> |
| 59 |
); |
| 60 |
})} |
| 61 |
</div> |
| 62 |
); |
| 63 |
}; |
| 64 |
|
| 65 |
return ( |
| 66 |
<div className="w-full" style={{ height: 180 }}> |
| 67 |
<ResponsiveContainer width="100%" height="100%"> |
| 68 |
<PieChart> |
| 69 |
<Pie |
| 70 |
data={chartData} |
| 71 |
dataKey="value" |
| 72 |
nameKey="label" |
| 73 |
cx="40%" |
| 74 |
cy="50%" |
| 75 |
innerRadius={50} |
| 76 |
outerRadius={70} |
| 77 |
paddingAngle={2} |
| 78 |
> |
| 79 |
{chartData.map((entry, index) => ( |
| 80 |
<Cell key={`cell-${index}`} fill={entry.color} /> |
| 81 |
))} |
| 82 |
</Pie> |
| 83 |
<Tooltip |
| 84 |
formatter={(_value: any, _name: any, props: any) => |
| 85 |
`${props.payload.percent.toFixed(1)}%` |
| 86 |
} |
| 87 |
contentStyle={{ fontFamily: "inherit", fontSize: 11 }} |
| 88 |
/> |
| 89 |
<Legend |
| 90 |
verticalAlign="middle" |
| 91 |
align="right" |
| 92 |
layout="vertical" |
| 93 |
iconType="circle" |
| 94 |
wrapperStyle={{ fontFamily: "inherit", fontSize: 11 }} |
| 95 |
content={renderLegend} |
| 96 |
/> |
| 97 |
</PieChart> |
| 98 |
</ResponsiveContainer> |
| 99 |
</div> |
| 100 |
); |
| 101 |
}; |
| 102 |
|
| 103 |
export default BookingStatusChart; |
| 104 |
|